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.
626 lines
26 KiB
TypeScript
626 lines
26 KiB
TypeScript
"use client"
|
|
|
|
/**
|
|
* /careers — the openings, the terms, and how hiring runs.
|
|
*
|
|
* NO TAILWIND. The four section frames are `components/marketing/band`'s `Band`
|
|
* and `Head`; @hanzo/ui's marketing components carry the type and the controls
|
|
* (`Display`, `Lede`, `Title`, `Chip`, `Action`, `Leaf`, `Line`); everything
|
|
* left is gui props over @hanzo/design's tokens.
|
|
*/
|
|
|
|
import { useState, useEffect, type FormEvent } from "react"
|
|
import { motion } from "@/components/motion"
|
|
import {
|
|
Briefcase,
|
|
Globe,
|
|
Clock,
|
|
MapPin,
|
|
Users,
|
|
Sparkles,
|
|
ArrowRight,
|
|
Heart,
|
|
CheckCircle2,
|
|
X,
|
|
Share2,
|
|
Check,
|
|
ExternalLink,
|
|
} from "lucide-react"
|
|
import { Box, Button, Dialog, DialogContent, DialogTitle, Input, Text, Textarea, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { Band, Head } from '@/components/marketing/band'
|
|
import { Action, Chip, Display, Lede, Title, Leaf, Lift, Line, type LineProps } from '@hanzo/ui/marketing'
|
|
import { api } from '@hanzo/app/lib/api'
|
|
import { track } from '@hanzo/app/lib/tags'
|
|
import { jobOpenings, getJobOpening, type JobOpening } from '@/lib/careers'
|
|
import { ResumeAndConsent, resumeProblem, uploadResume } from './apply'
|
|
|
|
/** The two inks this page draws body copy in. */
|
|
const INK = 'var(--foreground)'
|
|
const DIM = 'var(--muted-foreground)'
|
|
|
|
/** The pair of controls that closes a section, wrapping on a phone. */
|
|
const ROW = { flexWrap: 'wrap', items: 'center', justify: 'center', gap: 16 } as const
|
|
|
|
/** The hairline tile, centred. */
|
|
const CARD = { rounded: 'var(--radius-xl)', borderWidth: 1, borderColor: 'var(--border)', bg: 'var(--secondary)', p: 24 } as const
|
|
|
|
/** A card's heading and the paragraph under it. */
|
|
const CardTitle = (p: LineProps) => (
|
|
<Line render="h3" display="block" mb={8} fontSize="$4" lineHeight="1.5556" fontWeight="600" color={INK} {...p} />
|
|
)
|
|
const CardBody = (p: LineProps) => (
|
|
<Line render="p" display="block" fontSize="$2" lineHeight="1.4286" color={DIM} {...p} />
|
|
)
|
|
|
|
/** A job card's own frame: `Leaf` with its edge drawn at rest and 24px inside. */
|
|
const OPENING = { p: 24, borderColor: "var(--border)", bg: "var(--secondary)" } as const
|
|
|
|
/** An icon on a tinted tile — the mark that opens a card. */
|
|
const TILE = {
|
|
width: 48,
|
|
height: 48,
|
|
items: 'center',
|
|
justify: 'center',
|
|
rounded: 'var(--radius-lg)',
|
|
'$platform-web': { backgroundColor: 'color-mix(in oklab, var(--pure-white) 10%, transparent)' },
|
|
} as const
|
|
|
|
/** The one fact line beside a job: a mark and a word. */
|
|
const Fact = (p: LineProps) => (
|
|
<Line display="inline-flex" items="center" gap={4} whiteSpace="normal" fontSize="$2" lineHeight="1.4286" color={DIM} {...p} />
|
|
)
|
|
|
|
const benefits = [
|
|
{ icon: Globe, title: "Remote-first", description: "The team is distributed. Work from wherever you already live." },
|
|
{ icon: Clock, title: "Your own hours", description: "We care what you ship, not what time you were online." },
|
|
{ icon: Users, title: "Small team", description: "Few enough people that you own whole systems rather than tickets." },
|
|
{ icon: Sparkles, title: "The work is public", description: "Most of what we build is open source, under your own name." },
|
|
]
|
|
|
|
const hiringSteps = [
|
|
{ step: "1", title: "Application Review", description: "We review your resume and application materials" },
|
|
{ step: "2", title: "Initial Interview", description: "Get to know you and your experience" },
|
|
{ step: "3", title: "Technical Assessment", description: "Showcase your skills (no whiteboarding)" },
|
|
{ step: "4", title: "Final Interview", description: "Meet the team and discuss next steps" },
|
|
]
|
|
|
|
/** An address as https: one typed without a scheme gets https://, and cloud drops any other. */
|
|
const https = (u: string): string => {
|
|
const t = u.trim()
|
|
return !t || /^[a-z][a-z0-9+.-]*:/i.test(t) ? t : `https://${t}`
|
|
}
|
|
|
|
export default function CareersPage() {
|
|
const [activeJob, setActiveJob] = useState<JobOpening | null>(null)
|
|
const [copiedId, setCopiedId] = useState<string | null>(null)
|
|
const [form, setForm] = useState({
|
|
name: '',
|
|
email: '',
|
|
github: '',
|
|
linkedin: '',
|
|
location: '',
|
|
resume: '',
|
|
notes: '',
|
|
})
|
|
const [file, setFile] = useState<File | null>(null)
|
|
const [consent, setConsent] = useState(false)
|
|
const [busy, setBusy] = useState(false)
|
|
const [sent, setSent] = useState(false)
|
|
const [wrong, setWrong] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return
|
|
const params = new URLSearchParams(window.location.search)
|
|
const applyId = params.get('apply')
|
|
if (applyId) {
|
|
const match = getJobOpening(applyId)
|
|
if (match) setActiveJob(match)
|
|
}
|
|
}, [])
|
|
|
|
const openApply = (job: JobOpening) => {
|
|
setActiveJob(job)
|
|
setSent(false)
|
|
setWrong(null)
|
|
if (typeof window !== 'undefined') {
|
|
history.pushState(null, '', `?apply=${job.id}`)
|
|
}
|
|
}
|
|
|
|
const closeApply = () => {
|
|
setActiveJob(null)
|
|
setSent(false)
|
|
setWrong(null)
|
|
if (typeof window !== 'undefined') {
|
|
const url = new URL(window.location.href)
|
|
url.searchParams.delete('apply')
|
|
history.pushState(null, '', url.pathname + (url.hash || ''))
|
|
}
|
|
}
|
|
|
|
const copyRoleLink = (jobId: string, e: React.MouseEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
if (typeof window !== 'undefined') {
|
|
const origin = window.location.origin
|
|
navigator.clipboard.writeText(`${origin}/careers/${jobId}`)
|
|
setCopiedId(jobId)
|
|
setTimeout(() => setCopiedId(null), 2000)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault()
|
|
if (busy) return
|
|
if (!form.name.trim() || !form.email.trim() || !form.notes.trim()) {
|
|
setWrong('Please fill in your name, email, and cover note / background.')
|
|
return
|
|
}
|
|
const EMAIL_TEST = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
if (!EMAIL_TEST.test(form.email.trim())) {
|
|
setWrong('Please enter a valid email address.')
|
|
return
|
|
}
|
|
if (file && resumeProblem(file)) {
|
|
setWrong(resumeProblem(file))
|
|
return
|
|
}
|
|
setWrong(null)
|
|
setBusy(true)
|
|
|
|
let resumeFile = ''
|
|
try {
|
|
if (file) resumeFile = await uploadResume(file)
|
|
} catch {
|
|
setWrong('Your resume did not upload. Please try again, or give a link instead.')
|
|
setBusy(false)
|
|
return
|
|
}
|
|
const payload = {
|
|
name: form.name.trim(),
|
|
email: form.email.trim(),
|
|
role: activeJob?.title || 'Open Application',
|
|
department: activeJob?.department || 'General',
|
|
country: form.location.trim() || activeJob?.location || 'Remote',
|
|
source: `hanzo.ai/careers/${activeJob?.id || 'open'}`,
|
|
// An application to an opening is a candidate in the CRM (cloud apps/marketing
|
|
// leads.go `job`): what the person wrote, their resume and their links, with no
|
|
// call offered and nothing sent to the ad platforms.
|
|
job: activeJob?.id || 'open',
|
|
need: form.notes.trim(),
|
|
resume: https(form.resume),
|
|
links: [https(form.github), https(form.linkedin)].filter(Boolean),
|
|
resume_file: resumeFile,
|
|
consent,
|
|
}
|
|
|
|
try {
|
|
const r = await fetch(`${api()}/v1/marketing/leads`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
|
track('career_application_submitted', { role: payload.role, department: payload.department, slug: activeJob?.id })
|
|
setSent(true)
|
|
} catch (err) {
|
|
track('career_application_submitted', { role: payload.role, department: payload.department, slug: activeJob?.id, error: String(err) })
|
|
setWrong('Your application did not reach us. Please try again, or email it to [email protected].')
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Box minH="100vh" $platform-web={{ color: INK }}>
|
|
<main>
|
|
{/* Hero Section */}
|
|
<Box
|
|
render="section"
|
|
position="relative"
|
|
overflow="hidden"
|
|
pt={96}
|
|
pb={64}
|
|
px={16}
|
|
$md={{ px: 32 }}
|
|
$lg={{ px: 48 }}
|
|
>
|
|
<View
|
|
aria-hidden
|
|
position="absolute"
|
|
t={0}
|
|
l="50%"
|
|
x="-50%"
|
|
width={800}
|
|
height={400}
|
|
backgroundImage="radial-gradient(circle, color-mix(in srgb, var(--primary) 12%, transparent) 0%, transparent 70%)"
|
|
filter="blur(80px)"
|
|
pointerEvents="none"
|
|
/>
|
|
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1024} mx="auto">
|
|
<Box $platform-web={{ textAlign: 'center' }}>
|
|
<Chip render={<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4 }} />} mb={24} px={12} py={4} fontSize="$1" lineHeight="1.3333" fontWeight="500" borderColor="var(--border)" bg="color-mix(in oklab, var(--pure-white) 20%, transparent)" color={INK}>
|
|
<Heart width={14} height={14} />
|
|
Careers
|
|
</Chip>
|
|
|
|
<Display render={<motion.h1 initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.05 }} />} mb={24}>
|
|
Compilers, cryptography,<br />
|
|
and the cloud they run on.
|
|
</Display>
|
|
|
|
<Lede render={<motion.p initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.1 }} />} mb={40} width="100%" maxW={768} mx="auto">
|
|
Hanzo is a small team building an open AI cloud — the inference engine, the models, the backend services, and the agents that use them. Most of what we write is public, so you can read the codebase before you apply.
|
|
</Lede>
|
|
|
|
<XStack render={<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.15 }} />} {...ROW}>
|
|
<Action href="#open-positions" fill>
|
|
View Open Positions
|
|
<ArrowRight width={16} height={16} />
|
|
</Action>
|
|
<Action href="/about">
|
|
Read the history
|
|
</Action>
|
|
</XStack>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Benefits Section */}
|
|
<Band rule={false} pad="$10" measure={1152} ground="color-mix(in oklab, var(--secondary) 20%, transparent)">
|
|
<Head title="Benefits & Perks" lede="How the team actually works." />
|
|
<Grid columns={{ min: 260, max: 4 }} gap={24}>
|
|
{benefits.map((benefit, index) => {
|
|
const Icon = benefit.icon
|
|
return (
|
|
<Box
|
|
key={benefit.title}
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: index * 0.1 }} />}
|
|
{...CARD}
|
|
$platform-web={{ textAlign: 'center' }}
|
|
>
|
|
<XStack mb={16} mx="auto" {...TILE}>
|
|
<Icon width={24} height={24} color={INK} />
|
|
</XStack>
|
|
<CardTitle>{benefit.title}</CardTitle>
|
|
<CardBody>{benefit.description}</CardBody>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{/* Open Positions Section */}
|
|
<Band id="open-positions" rule={false} pad="$10" measure={1152}>
|
|
<Head title="Open Positions" lede="Remote, unless the work needs hardware in a room." />
|
|
<Grid columns={{ min: 320, max: 2 }} gap={24}>
|
|
{jobOpenings.map((job, index) => (
|
|
<Leaf
|
|
id={job.id}
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: index * 0.05 }} />}
|
|
key={job.id}
|
|
{...OPENING}
|
|
>
|
|
<XStack items="flex-start" gap={16}>
|
|
<XStack shrink={0} {...TILE}>
|
|
<Briefcase width={24} height={24} color={INK} />
|
|
</XStack>
|
|
<YStack grow={1} shrink={1} flexBasis="0%">
|
|
<XStack items="center" justify="space-between" mb={8} gap={8}>
|
|
<Line href={`/careers/${job.id}`} color="inherit" whiteSpace="inherit">
|
|
<Lift render="h3" display="block" fontSize="$6" lineHeight="1.4" fontWeight="700">
|
|
{job.title}
|
|
</Lift>
|
|
</Line>
|
|
<Line
|
|
render="button"
|
|
type="button"
|
|
onClick={(e) => copyRoleLink(job.id, e)}
|
|
title="Copy direct link to this position"
|
|
display="inline-flex"
|
|
items="center"
|
|
p={4}
|
|
color={copiedId === job.id ? 'var(--primary)' : DIM}
|
|
>
|
|
{copiedId === job.id ? <Check size={16} /> : <Share2 size={16} />}
|
|
</Line>
|
|
</XStack>
|
|
|
|
<XStack gap={12} mb={12} flexWrap="wrap">
|
|
<Fact><MapPin width={16} height={16} /> {job.location}</Fact>
|
|
<Fact><Clock width={16} height={16} /> {job.type}</Fact>
|
|
<Fact><Users width={16} height={16} /> {job.department}</Fact>
|
|
</XStack>
|
|
<CardBody mb={16}>{job.description}</CardBody>
|
|
|
|
<XStack gap={12} flexWrap="wrap" items="center">
|
|
<Action
|
|
href={`/careers/${job.id}`}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
openApply(job)
|
|
}}
|
|
fill
|
|
self="flex-start"
|
|
>
|
|
Apply for this role
|
|
<ArrowRight width={16} height={16} />
|
|
</Action>
|
|
<Action
|
|
href={`/careers/${job.id}`}
|
|
self="flex-start"
|
|
>
|
|
View Details
|
|
<ExternalLink width={14} height={14} />
|
|
</Action>
|
|
</XStack>
|
|
</YStack>
|
|
</XStack>
|
|
</Leaf>
|
|
))}
|
|
</Grid>
|
|
<Box render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} />} mt={32} $platform-web={{ textAlign: 'center' }}>
|
|
<Line render="p" display="block" mb={16} color={DIM}>
|
|
Don't see a position that fits your skills?
|
|
</Line>
|
|
<Action
|
|
href="/careers/open"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
const openRole = getJobOpening('open')
|
|
if (openRole) openApply(openRole)
|
|
}}
|
|
>
|
|
Submit Open Application
|
|
</Action>
|
|
</Box>
|
|
</Band>
|
|
|
|
{/* Hiring Process Section */}
|
|
<Band rule={false} pad="$10" measure={896} ground="color-mix(in oklab, var(--secondary) 20%, transparent)">
|
|
<Head title="Our Hiring Process" lede="No whiteboarding. Here is the whole thing." />
|
|
<Grid columns={{ min: 180, max: 4 }} gap={24}>
|
|
{hiringSteps.map((step, index) => (
|
|
<Box
|
|
key={step.step}
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: index * 0.1 }} />}
|
|
$platform-web={{ textAlign: 'center' }}
|
|
>
|
|
<XStack
|
|
mb={16}
|
|
mx="auto"
|
|
{...TILE}
|
|
rounded="var(--radius-full)"
|
|
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--pure-white) 20%, transparent)' }}
|
|
>
|
|
<Line fontWeight="700" color={INK}>
|
|
{step.step}
|
|
</Line>
|
|
</XStack>
|
|
<CardTitle>{step.title}</CardTitle>
|
|
<CardBody>{step.description}</CardBody>
|
|
</Box>
|
|
))}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{/* CTA Section */}
|
|
<Band rule={false} pad="$12" measure={896}>
|
|
<View
|
|
position="absolute"
|
|
t={-160}
|
|
r={-160}
|
|
width={320}
|
|
height={320}
|
|
rounded="var(--radius-full)"
|
|
filter="blur(64px)"
|
|
pointerEvents="none"
|
|
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--pure-white) 10%, transparent)' }}
|
|
/>
|
|
<View
|
|
position="absolute"
|
|
b={-160}
|
|
l={-160}
|
|
width={320}
|
|
height={320}
|
|
rounded="var(--radius-full)"
|
|
filter="blur(64px)"
|
|
pointerEvents="none"
|
|
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--pure-white) 5%, transparent)' }}
|
|
/>
|
|
<Box position="relative" z="var(--z-raised)" $platform-web={{ textAlign: 'center' }}>
|
|
<Title render={<motion.h2 initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} />} quiet mb={16}>Ready to join us?</Title>
|
|
<Lede render={<motion.p initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.1 }} />} mb={40} width="100%" maxW={672} mx="auto" mt="$3.5">
|
|
If you have read the codebase and had opinions about it, we want to hear them.
|
|
</Lede>
|
|
<XStack render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.2 }} />} {...ROW}>
|
|
<Action href="#open-positions" fill>
|
|
Browse Open Positions
|
|
<ArrowRight width={20} height={20} />
|
|
</Action>
|
|
<Action href="mailto:[email protected]">
|
|
careers@hanzo.ai
|
|
</Action>
|
|
</XStack>
|
|
</Box>
|
|
</Band>
|
|
|
|
<Dialog open={Boolean(activeJob)} onOpenChange={(o) => (!o ? closeApply() : undefined)}>
|
|
<DialogContent
|
|
showCloseButton={false}
|
|
width="calc(100vw - 32px)"
|
|
maxW={540}
|
|
rounded="$6"
|
|
p="$5"
|
|
gap="$4"
|
|
bg="var(--secondary)"
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
>
|
|
<XStack items="center" justify="space-between" width="100%">
|
|
<YStack gap="$1">
|
|
<DialogTitle fontSize="$6" fontWeight="700" color={INK}>
|
|
{sent ? 'Application Received' : activeJob?.title}
|
|
</DialogTitle>
|
|
{!sent && activeJob ? (
|
|
<Text fontSize="$2" color={DIM}>
|
|
{activeJob.department} · {activeJob.location} · {activeJob.type}
|
|
</Text>
|
|
) : null}
|
|
</YStack>
|
|
<XStack
|
|
render="button"
|
|
aria-label="Close"
|
|
p="$1.5"
|
|
rounded="$3"
|
|
hoverStyle={{ bg: 'var(--white-06)' }}
|
|
cursor="pointer"
|
|
onPress={closeApply}
|
|
>
|
|
<X size={18} color={INK} />
|
|
</XStack>
|
|
</XStack>
|
|
|
|
{sent ? (
|
|
<YStack gap="$4" py="$4" items="center">
|
|
<CheckCircle2 size={48} color="var(--primary)" />
|
|
<YStack gap="$2" items="center">
|
|
<Text fontSize="$4" fontWeight="600" color={INK}>
|
|
Thank you for applying!
|
|
</Text>
|
|
<Text fontSize="$2" color={DIM} maxW={420} $platform-web={{ textAlign: 'center' }}>
|
|
Your application for <Text fontWeight="600" color={INK}>{activeJob?.title}</Text> has been submitted to our team. We review submissions promptly and will reach out to <Text fontWeight="600" color={INK}>{form.email}</Text>.
|
|
</Text>
|
|
<Text fontSize="$1" color={DIM} mt="$2" $platform-web={{ textAlign: 'center' }}>
|
|
You can also email additional materials or portfolio samples directly to{' '}
|
|
<Text render="a" {...{ href: `mailto:[email protected]?subject=${encodeURIComponent(`Application: ${activeJob?.title} - ${form.name}`)}` }} color="var(--primary)" textDecorationLine="underline">
|
|
careers@hanzo.ai
|
|
</Text>.
|
|
</Text>
|
|
</YStack>
|
|
<Button variant="primary" onPress={closeApply} mt="$2">
|
|
Done
|
|
</Button>
|
|
</YStack>
|
|
) : (
|
|
<YStack render="form" {...{ onSubmit: handleSubmit }} width="100%" gap={14}>
|
|
<YStack gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
Full Name <Text render="span" color="var(--destructive)">*</Text>
|
|
</Text>
|
|
<Input
|
|
value={form.name}
|
|
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
|
placeholder="Ada Lovelace"
|
|
required
|
|
/>
|
|
</YStack>
|
|
|
|
<YStack gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
Work Email <Text render="span" color="var(--destructive)">*</Text>
|
|
</Text>
|
|
<Input
|
|
type="email"
|
|
value={form.email}
|
|
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
|
placeholder="[email protected]"
|
|
required
|
|
/>
|
|
</YStack>
|
|
|
|
<XStack gap="$3" flexWrap="wrap">
|
|
<YStack flex={1} minW={200} gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
GitHub / Portfolio
|
|
</Text>
|
|
<Input
|
|
value={form.github}
|
|
onChange={(e) => setForm((f) => ({ ...f, github: e.target.value }))}
|
|
placeholder="https://github.com/..."
|
|
/>
|
|
</YStack>
|
|
<YStack flex={1} minW={200} gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
LinkedIn / Website
|
|
</Text>
|
|
<Input
|
|
value={form.linkedin}
|
|
onChange={(e) => setForm((f) => ({ ...f, linkedin: e.target.value }))}
|
|
placeholder="https://linkedin.com/in/..."
|
|
/>
|
|
</YStack>
|
|
</XStack>
|
|
|
|
<YStack gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
Resume link
|
|
</Text>
|
|
<Input
|
|
value={form.resume}
|
|
onChange={(e) => setForm((f) => ({ ...f, resume: e.target.value }))}
|
|
placeholder="A link to your resume: PDF, Google Drive or Dropbox"
|
|
/>
|
|
</YStack>
|
|
|
|
<ResumeAndConsent file={file} onFile={setFile} consent={consent} onConsent={setConsent} />
|
|
|
|
<YStack gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
Location / Timezone
|
|
</Text>
|
|
<Input
|
|
value={form.location}
|
|
onChange={(e) => setForm((f) => ({ ...f, location: e.target.value }))}
|
|
placeholder="e.g. San Francisco, CA or Remote (UTC-8)"
|
|
/>
|
|
</YStack>
|
|
|
|
<YStack gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
Background & Why Hanzo <Text render="span" color="var(--destructive)">*</Text>
|
|
</Text>
|
|
<Textarea
|
|
value={form.notes}
|
|
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
|
|
placeholder="Briefly tell us what systems you've built, open source work, or what interests you about our cloud and models."
|
|
rows={4}
|
|
required
|
|
/>
|
|
</YStack>
|
|
|
|
{wrong ? (
|
|
<Text fontSize="$2" color="var(--destructive)">
|
|
{wrong}
|
|
</Text>
|
|
) : null}
|
|
|
|
<XStack justify="space-between" items="center" mt="$2">
|
|
<Text
|
|
render="a"
|
|
{...{ href: `mailto:[email protected]?subject=${encodeURIComponent(`Application: ${activeJob?.title}`)}` }}
|
|
fontSize="$1"
|
|
color="var(--muted-foreground)"
|
|
textDecorationLine="underline"
|
|
>
|
|
Or email resume directly
|
|
</Text>
|
|
<XStack gap="$2">
|
|
<Button variant="ghost" size="sm" type="button" onPress={closeApply}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="primary" size="sm" type="submit" disabled={busy}>
|
|
{busy ? 'Submitting…' : 'Submit Application'}
|
|
</Button>
|
|
</XStack>
|
|
</XStack>
|
|
</YStack>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</main>
|
|
</Box>
|
|
)
|
|
}
|