Files
hanzo.ai/app/(marketing)/careers/apply.tsx
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

83 lines
3.2 KiB
TypeScript

'use client'
/**
* What both careers forms add to an application: a resume FILE and the one opt-in.
*
* The file goes up before the application, straight to Hanzo's private careers
* bucket through a form cloud signs for that one file (POST /v1/marketing/leads/resume:
* a PDF, DOC or DOCX of at most 10 MB); the application then names its key as
* `resume_file`. A link still works instead. The box is unticked: only an applicant who
* ticks it is kept posted (`consent`).
*/
import { Text, XStack, YStack } from '@hanzo/ui'
import { api } from '@hanzo/app/lib/api'
const INK = 'var(--foreground)'
export const KEEP_POSTED = 'Keep me posted about roles and news at Hanzo. Unsubscribe any time.'
const TYPES: Record<string, string> = {
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}
/** Why a file cannot be a resume, or '' when it can. */
export function resumeProblem(f: File): string {
const t = TYPES[f.name.split('.').pop()?.toLowerCase() ?? '']
if (!t) return 'A resume is a PDF, DOC or DOCX file.'
if (f.size > 10 * 1024 * 1024) return 'A resume is at most 10 MB.'
return ''
}
/** Upload the resume and answer the key the application names. */
export async function uploadResume(f: File): Promise<string> {
const type = TYPES[f.name.split('.').pop()?.toLowerCase() ?? '']
const r = await fetch(`${api()}/v1/marketing/leads/resume`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: f.name, type, size: f.size }),
})
if (!r.ok) throw new Error(`resume form HTTP ${r.status}`)
const form = (await r.json()) as { url: string; fields: Record<string, string>; key: string }
const body = new FormData()
for (const [k, v] of Object.entries(form.fields)) body.append(k, v)
body.append('file', new Blob([f], { type }), f.name)
const up = await fetch(form.url, { method: 'POST', body })
if (!up.ok) throw new Error(`resume upload HTTP ${up.status}`)
return form.key
}
/** The file picker and the opt-in, as both forms show them. */
export function ResumeAndConsent({ file, onFile, consent, onConsent }: {
file: File | null
onFile: (f: File | null) => void
consent: boolean
onConsent: (on: boolean) => void
}) {
return (
<YStack gap="$3">
<YStack gap="$1">
<Text fontSize="$2" fontWeight="500" color={INK}>Resume file</Text>
<Text
render={
<input
type="file"
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
aria-label="Resume file"
onChange={(e) => onFile(e.target.files?.[0] ?? null)}
/>
}
color="inherit"
fontSize="$3"
/>
<Text fontSize="$1" opacity={0.7} color={INK}>{file ? file.name : 'PDF, DOC or DOCX, up to 10 MB. Or give a link above.'}</Text>
</YStack>
<XStack gap="$2" items="center" render="label">
<input type="checkbox" checked={consent} onChange={(e) => onConsent(e.target.checked)} aria-label={KEEP_POSTED} />
<Text fontSize="$2" color={INK}>{KEEP_POSTED}</Text>
</XStack>
</YStack>
)
}