mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
The bar is Models, Dev, Platform, Pricing, Docs, Enterprise, Log in, and Get API key. The homepage is seven sections. Launch pages stay in the tree and drop out of the index through lib/launch.ts. /platform forwards to /cloud. Model and repository floors come from the snapshots the build already keeps. Co-authored-by: z <[email protected]>
627 lines
26 KiB
TypeScript
627 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 { shownCareer } from '@/lib/launch'
|
|
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.filter((job) => shownCareer(job.id)).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>
|
|
)
|
|
}
|