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.
482 lines
19 KiB
TypeScript
482 lines
19 KiB
TypeScript
"use client"
|
|
|
|
import { useState, type FormEvent } from "react"
|
|
import { motion } from "@/components/motion"
|
|
import {
|
|
Briefcase,
|
|
Globe,
|
|
Clock,
|
|
MapPin,
|
|
Users,
|
|
Sparkles,
|
|
ArrowRight,
|
|
ArrowLeft,
|
|
CheckCircle2,
|
|
Copy,
|
|
Check,
|
|
} from "lucide-react"
|
|
import { Box, Button, Input, Text, Textarea, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Band } from '@/components/marketing/band'
|
|
import { Action, Chip, Display, Lede, Title, Line } from '@hanzo/ui/marketing'
|
|
import { api } from '@hanzo/app/lib/api'
|
|
import { track } from '@hanzo/app/lib/tags'
|
|
import type { JobOpening } from '@/lib/careers'
|
|
import { ResumeAndConsent, resumeProblem, uploadResume } from '../apply'
|
|
|
|
const INK = 'var(--foreground)'
|
|
const DIM = 'var(--muted-foreground)'
|
|
|
|
const SECTION_BOX = {
|
|
p: 28,
|
|
rounded: 'var(--radius-xl)',
|
|
borderWidth: 1,
|
|
borderColor: 'var(--border)',
|
|
bg: 'var(--secondary)',
|
|
} as const
|
|
|
|
/** 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 function JobView({ job }: { job: JobOpening }) {
|
|
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)
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
const copyLink = () => {
|
|
if (typeof window !== 'undefined') {
|
|
navigator.clipboard.writeText(window.location.href)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 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 background note.')
|
|
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: job.title,
|
|
department: job.department,
|
|
country: form.location.trim() || job.location,
|
|
source: `hanzo.ai/careers/${job.id}`,
|
|
// 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: job.id,
|
|
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: job.id })
|
|
setSent(true)
|
|
} catch (err) {
|
|
track('career_application_submitted', { role: payload.role, department: payload.department, slug: job.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>
|
|
{/* Top Header / Breadcrumb */}
|
|
<Box
|
|
render="section"
|
|
position="relative"
|
|
overflow="hidden"
|
|
pt={80}
|
|
pb={48}
|
|
px={16}
|
|
$md={{ px: 32 }}
|
|
$lg={{ px: 48 }}
|
|
>
|
|
<View
|
|
aria-hidden
|
|
position="absolute"
|
|
t={0}
|
|
l="50%"
|
|
x="-50%"
|
|
width={800}
|
|
height={360}
|
|
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">
|
|
<XStack items="center" justify="space-between" mb={24} flexWrap="wrap" gap={12}>
|
|
<Line href="/careers" display="inline-flex" items="center" gap={6} fontSize="$3" color={DIM}>
|
|
<ArrowLeft width={16} height={16} />
|
|
Back to all careers
|
|
</Line>
|
|
|
|
<Button variant="ghost" size="sm" onPress={copyLink}>
|
|
{copied ? <Check size={14} color="var(--primary)" /> : <Copy size={14} />}
|
|
{copied ? 'Link copied' : 'Share position'}
|
|
</Button>
|
|
</XStack>
|
|
|
|
<XStack gap={8} mb={16} flexWrap="wrap" items="center">
|
|
<Chip fontSize="$1" lineHeight="1.3333" fontWeight="500" borderColor="var(--border)" bg="color-mix(in oklab, var(--pure-white) 15%, transparent)" color={INK}>
|
|
<Briefcase width={13} height={13} />
|
|
{job.department}
|
|
</Chip>
|
|
<Chip fontSize="$1" lineHeight="1.3333" fontWeight="500" borderColor="var(--border)" bg="transparent" color={DIM}>
|
|
<MapPin width={13} height={13} />
|
|
{job.location}
|
|
</Chip>
|
|
<Chip fontSize="$1" lineHeight="1.3333" fontWeight="500" borderColor="var(--border)" bg="transparent" color={DIM}>
|
|
<Clock width={13} height={13} />
|
|
{job.type}
|
|
</Chip>
|
|
</XStack>
|
|
|
|
<Display render={<motion.h1 initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4 }} />} mb={16}>
|
|
{job.title}
|
|
</Display>
|
|
|
|
<Lede render={<motion.p initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.05 }} />} mb={32} width="100%" maxW={768}>
|
|
{job.description}
|
|
</Lede>
|
|
|
|
<XStack gap={16} flexWrap="wrap" items="center">
|
|
<Action href="#apply" fill>
|
|
Apply for this position
|
|
<ArrowRight width={16} height={16} />
|
|
</Action>
|
|
<Action href={`mailto:[email protected]?subject=${encodeURIComponent(`Application: ${job.title}`)}`}>
|
|
Email resume directly
|
|
</Action>
|
|
</XStack>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Content Body Grid */}
|
|
<Band rule={false} pad="$10" measure={1024}>
|
|
<Box display="grid" gap={32} gridTemplateColumns="minmax(0, 1fr)" $lg={{ gridTemplateColumns: 'minmax(0, 1.4fr) minmax(0, 1fr)', items: 'flex-start' }}>
|
|
{/* Left: Job Description & Details */}
|
|
<YStack gap={32}>
|
|
<Box {...SECTION_BOX}>
|
|
<Title quiet mb={16} fontSize="$5" fontWeight="600">
|
|
About the Role
|
|
</Title>
|
|
<Line render="p" display="block" fontSize="$3" lineHeight="1.6" color={INK} mb={16}>
|
|
{job.about}
|
|
</Line>
|
|
<Line render="p" display="block" fontSize="$2" lineHeight="1.5" color={DIM}>
|
|
Hanzo builds an open AI cloud — compilers, models, distributed infrastructure, and agent tooling. Our codebases are predominantly open source, and team members operate with high autonomy and end-to-end ownership.
|
|
</Line>
|
|
</Box>
|
|
|
|
<Box {...SECTION_BOX}>
|
|
<Title quiet mb={16} fontSize="$5" fontWeight="600">
|
|
Key Responsibilities
|
|
</Title>
|
|
<YStack gap={12}>
|
|
{job.responsibilities.map((r, i) => (
|
|
<XStack key={i} items="flex-start" gap={10}>
|
|
<View
|
|
width={6}
|
|
height={6}
|
|
rounded="var(--radius-full)"
|
|
bg="var(--primary)"
|
|
mt={8}
|
|
shrink={0}
|
|
/>
|
|
<Line render="p" fontSize="$2" lineHeight="1.5" color={INK}>
|
|
{r}
|
|
</Line>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
</Box>
|
|
|
|
<Box {...SECTION_BOX}>
|
|
<Title quiet mb={16} fontSize="$5" fontWeight="600">
|
|
What We're Looking For
|
|
</Title>
|
|
<YStack gap={12}>
|
|
{job.requirements.map((req, i) => (
|
|
<XStack key={i} items="flex-start" gap={10}>
|
|
<View
|
|
width={6}
|
|
height={6}
|
|
rounded="var(--radius-full)"
|
|
bg="var(--primary)"
|
|
mt={8}
|
|
shrink={0}
|
|
/>
|
|
<Line render="p" fontSize="$2" lineHeight="1.5" color={INK}>
|
|
{req}
|
|
</Line>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
|
|
{job.niceToHave && job.niceToHave.length > 0 ? (
|
|
<>
|
|
<Title quiet mt={24} mb={12} fontSize="$4" fontWeight="600">
|
|
Nice to Have
|
|
</Title>
|
|
<YStack gap={12}>
|
|
{job.niceToHave.map((nth, i) => (
|
|
<XStack key={i} items="flex-start" gap={10}>
|
|
<View
|
|
width={6}
|
|
height={6}
|
|
rounded="var(--radius-full)"
|
|
bg="var(--border)"
|
|
mt={8}
|
|
shrink={0}
|
|
/>
|
|
<Line render="p" fontSize="$2" lineHeight="1.5" color={DIM}>
|
|
{nth}
|
|
</Line>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
</>
|
|
) : null}
|
|
</Box>
|
|
|
|
<Box {...SECTION_BOX}>
|
|
<Title quiet mb={16} fontSize="$5" fontWeight="600">
|
|
Why Join Hanzo
|
|
</Title>
|
|
<YStack gap={12}>
|
|
<XStack items="flex-start" gap={12}>
|
|
<View render={<Globe size={18} color="var(--primary)" />} mt={2} shrink={0} />
|
|
<YStack flex={1} minW={0}>
|
|
<Line fontWeight="600" fontSize="$2" color={INK}>Remote-first culture</Line>
|
|
<Line fontSize="$2" color={DIM}>Distributed globally. We value outcomes over hours spent at a desk.</Line>
|
|
</YStack>
|
|
</XStack>
|
|
<XStack items="flex-start" gap={12}>
|
|
<View render={<Sparkles size={18} color="var(--primary)" />} mt={2} shrink={0} />
|
|
<YStack flex={1} minW={0}>
|
|
<Line fontWeight="600" fontSize="$2" color={INK}>Public, open-source output</Line>
|
|
<Line fontSize="$2" color={DIM}>Ship work that developers worldwide inspect, clone, and build on.</Line>
|
|
</YStack>
|
|
</XStack>
|
|
<XStack items="flex-start" gap={12}>
|
|
<View render={<Users size={18} color="var(--primary)" />} mt={2} shrink={0} />
|
|
<YStack flex={1} minW={0}>
|
|
<Line fontWeight="600" fontSize="$2" color={INK}>High agency & ownership</Line>
|
|
<Line fontSize="$2" color={DIM}>Direct access to leadership and the autonomy to build whole systems.</Line>
|
|
</YStack>
|
|
</XStack>
|
|
</YStack>
|
|
</Box>
|
|
</YStack>
|
|
|
|
{/* Right: Application Form */}
|
|
<Box id="apply" {...SECTION_BOX} position="sticky" t={96}>
|
|
<Title quiet mb={8} fontSize="$5" fontWeight="700">
|
|
Apply for this Role
|
|
</Title>
|
|
<Text fontSize="$2" color={DIM} mb={20} display="block">
|
|
Submit your information below to be directly reviewed by our team.
|
|
</Text>
|
|
|
|
{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}>
|
|
Application Received!
|
|
</Text>
|
|
<Text fontSize="$2" color={DIM} $platform-web={{ textAlign: 'center' }}>
|
|
Thank you for applying for <Text fontWeight="600" color={INK}>{job.title}</Text>. Our team reviews submissions promptly and will contact you at <Text fontWeight="600" color={INK}>{form.email}</Text>.
|
|
</Text>
|
|
<Text fontSize="$1" color={DIM} mt="$2" $platform-web={{ textAlign: 'center' }}>
|
|
You can also email your resume or portfolio directly to{' '}
|
|
<Line
|
|
href={`mailto:[email protected]?subject=${encodeURIComponent(`Application: ${job.title} - ${form.name}`)}`}
|
|
color="var(--primary)"
|
|
textDecorationLine="underline"
|
|
>
|
|
careers@hanzo.ai
|
|
</Line>.
|
|
</Text>
|
|
</YStack>
|
|
<Button
|
|
variant="primary"
|
|
onPress={() => setSent(false)}
|
|
mt="$2"
|
|
>
|
|
Submit Another Application
|
|
</Button>
|
|
</YStack>
|
|
) : (
|
|
<YStack render="form" {...{ onSubmit: handleSubmit }} 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}>
|
|
Email Address <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>
|
|
|
|
<YStack gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
GitHub / Portfolio URL
|
|
</Text>
|
|
<Input
|
|
value={form.github}
|
|
onChange={(e) => setForm((f) => ({ ...f, github: e.target.value }))}
|
|
placeholder="https://github.com/adalovelace"
|
|
/>
|
|
</YStack>
|
|
|
|
<YStack gap="$1">
|
|
<Text fontSize="$2" fontWeight="500" color={INK}>
|
|
LinkedIn / Personal Site
|
|
</Text>
|
|
<Input
|
|
value={form.linkedin}
|
|
onChange={(e) => setForm((f) => ({ ...f, linkedin: e.target.value }))}
|
|
placeholder="https://linkedin.com/in/ada"
|
|
/>
|
|
</YStack>
|
|
|
|
<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 & Notes <Text render="span" color="var(--destructive)">*</Text>
|
|
</Text>
|
|
<Textarea
|
|
value={form.notes}
|
|
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
|
|
placeholder="Tell us what systems you've built, relevant experience, or why you're interested in this role."
|
|
rows={5}
|
|
required
|
|
/>
|
|
</YStack>
|
|
|
|
{wrong ? (
|
|
<Text fontSize="$2" color="var(--destructive)">
|
|
{wrong}
|
|
</Text>
|
|
) : null}
|
|
|
|
<Button
|
|
variant="primary"
|
|
size="lg"
|
|
type="submit"
|
|
disabled={busy}
|
|
width="100%"
|
|
mt="$2"
|
|
>
|
|
{busy ? 'Submitting Application…' : 'Submit Application'}
|
|
</Button>
|
|
|
|
<Text fontSize="$1" color={DIM} $platform-web={{ textAlign: 'center' }} mt="$1">
|
|
Prefer email? Send your resume to{' '}
|
|
<Line
|
|
href={`mailto:[email protected]?subject=${encodeURIComponent(`Application: ${job.title}`)}`}
|
|
color="var(--primary)"
|
|
textDecorationLine="underline"
|
|
>
|
|
careers@hanzo.ai
|
|
</Line>
|
|
</Text>
|
|
</YStack>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Band>
|
|
</main>
|
|
</Box>
|
|
)
|
|
}
|