mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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.
83 lines
3.2 KiB
TypeScript
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>
|
|
)
|
|
}
|