mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
The owner's spec for a landing page is the models bento's: monochrome, the words left and the art right, no extra prose. The folds carried two to four sentences under each heading, and some a claim, a lede and a list besides. Each now says one line: ProductLanding the tagline where a page states one (the lede still describes it to search), a claim alone where the claim is the line (/payments, /treasury, /zap, the extension), and elsewhere the sentence that says what the thing is. /philosophy's two points, /dns' claim that repeated its title, /calculator's capabilities line and the Spare clauses on /research and /learn go.
901 lines
32 KiB
TypeScript
901 lines
32 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* /startups — the credits programme, and the form that applies to it.
|
||
*
|
||
* NO TAILWIND AND NO STYLE OBJECTS. @hanzo/ui's marketing components carry the
|
||
* type and the controls (`Display`, `Lede`, `Title`, `Chip`, `Action`), `Grid`
|
||
* wraps on the width a column actually has, and everything left is gui props
|
||
* over @hanzo/design's tokens: `Label` is the line over a field, `Card` a
|
||
* pillar, `Check` the box a choice ticks.
|
||
*
|
||
* NOT `Band`. This page's sections carry no rule, its gutter steps 16/32/48
|
||
* where every band on the site steps 16/24/32, and its rhythm (112/40, 32,
|
||
* 56, 96) is not on the band's ladder. A form page is not a product page, and
|
||
* borrowing the product band would restate all three.
|
||
*
|
||
* - `Field`/`Note` are gui's `Input`, because a placeholder's colour lives on
|
||
* `::placeholder` and `placeholderTextColor` is the prop that reaches it;
|
||
* without it the ink falls to @hanzo/design's `--text-disabled` (white at
|
||
* 30%) instead of gui's `$placeholderColor`, design's `--text-tertiary`.
|
||
* The two selects are @hanzo/ui's `Picker` in the same box.
|
||
* - `Cell` on `Toggle` — the site's surface whose ground answers a pointer.
|
||
* It adds no geometry, so an option in a multi-select wears it exactly as a
|
||
* grid tile does, and both grounds are its properties.
|
||
* - the privacy link says NOTHING. @hanzo/design's base already underlines a
|
||
* link inside a paragraph on approach, which is what every prose link on
|
||
* this site does; `More` is the wrong shape for one inside a sentence, since
|
||
* it is `inline-flex` at `text-sm` and stretching a link inside a line tears
|
||
* it.
|
||
*/
|
||
|
||
import React, { useState, type ReactNode } from 'react'
|
||
import { Fold } from '@/components/marketing/fold'
|
||
import { Steps } from '@/components/preview/steps'
|
||
import { motion } from '@/components/motion'
|
||
import Link from 'next/link'
|
||
import {
|
||
Rocket,
|
||
ArrowRight,
|
||
CheckCircle2,
|
||
Coins,
|
||
Cpu,
|
||
Gift,
|
||
Building2,
|
||
Globe,
|
||
User,
|
||
Mail,
|
||
Briefcase,
|
||
Users,
|
||
Banknote,
|
||
Boxes,
|
||
Send,
|
||
type LucideIcon,
|
||
} from 'lucide-react'
|
||
import { Box, Picker, Text, View, XStack, YStack } from '@hanzo/ui'
|
||
import { Input, type GetProps } from '@hanzo/gui'
|
||
import { Grid } from '@hanzo/ui/grid'
|
||
import { Action, Title, Cell, Line } from '@hanzo/ui/marketing'
|
||
import { mix } from '@hanzo/app/lib/mix'
|
||
import { api } from '@hanzo/app/lib/api'
|
||
import { phrase } from '@hanzo/i18n'
|
||
import { useI18n } from '@hanzo/i18n/react'
|
||
|
||
// Canonical cloud intake endpoint (public startup-program application), on the
|
||
// gateway of the host this page is open on.
|
||
const apply = () => `${api()}/v1/crm/applications`
|
||
|
||
/** How narrow a column may get before the grid drops one. */
|
||
const DUO = { min: 260, max: 2 } as const
|
||
|
||
const MUTED = 'var(--muted-foreground)'
|
||
|
||
/** The two small rungs, each on the leading its utility paired it with: a ratio,
|
||
* so it follows the reader's type scale. `Line` takes it as written. */
|
||
const XS = { fontSize: '$1', lineHeight: '1.3333' } as const
|
||
const SM = { fontSize: '$2', lineHeight: '1.4286' } as const
|
||
|
||
/** The line over a field: its mark and its words, `htmlFor` where it names one control. */
|
||
const Label = ({ htmlFor, icon: Icon, children }: { htmlFor?: string; icon: LucideIcon; children: ReactNode }) => (
|
||
<Line
|
||
render="label"
|
||
htmlFor={htmlFor}
|
||
display="flex"
|
||
items="center"
|
||
gap={8}
|
||
mb={8}
|
||
{...SM}
|
||
fontWeight="500"
|
||
color="var(--foreground)"
|
||
>
|
||
<View render={<Icon width={14} height={14} color={MUTED} />} shrink={0} />
|
||
{children}
|
||
</Line>
|
||
)
|
||
|
||
/** The quiet half of a label — "(optional)", "(funds, angels)". */
|
||
const Aside = ({ children }: { children: ReactNode }) => (
|
||
<Text fontWeight="400" color={MUTED}>
|
||
{children}
|
||
</Text>
|
||
)
|
||
|
||
/** The tick itself, drawn in the ground's ink. */
|
||
const Tick = ({ size }: { size: number }) => (
|
||
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" stroke="var(--background)" strokeWidth={3}>
|
||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
)
|
||
|
||
/** The box a choice ticks, at the two sizes this page ticks one: filled in the ink when on. */
|
||
const Check = ({ on, size, mt }: { on: boolean; size: 16 | 20; mt?: number }) => (
|
||
<View
|
||
render="span"
|
||
display="inline-flex"
|
||
width={size}
|
||
height={size}
|
||
shrink={0}
|
||
items="center"
|
||
justify="center"
|
||
mt={mt}
|
||
rounded="var(--radius)"
|
||
borderWidth={1}
|
||
borderStyle="solid"
|
||
transition="quickest"
|
||
bg={on ? 'var(--foreground)' : undefined}
|
||
borderColor={on ? 'var(--foreground)' : mix('var(--muted-foreground)', 40)}
|
||
>
|
||
{on && <Tick size={size === 16 ? 10 : 12} />}
|
||
</View>
|
||
)
|
||
|
||
/**
|
||
* One option in a multi-select, ticked or not.
|
||
*
|
||
* ONE ground on approach, for both the fund list and the use-case list — they
|
||
* were the same eleven utilities written twice, differing only in how far the
|
||
* tick sits from the top and how much room the row has.
|
||
*/
|
||
const Toggle = ({
|
||
on,
|
||
onClick,
|
||
align,
|
||
pad,
|
||
children,
|
||
}: {
|
||
on: boolean
|
||
onClick: () => void
|
||
align: 'center' | 'flex-start'
|
||
pad: number
|
||
children: React.ReactNode
|
||
}) => (
|
||
<Cell
|
||
render="button"
|
||
type="button"
|
||
onClick={onClick}
|
||
display="flex"
|
||
items={align}
|
||
gap={12}
|
||
p={pad}
|
||
rounded="var(--radius-lg)"
|
||
borderWidth={1}
|
||
borderStyle="solid"
|
||
text="left"
|
||
{...(on ? CHOSEN : CHOICE)}
|
||
>
|
||
<Check on={on} size={16} mt={align === 'flex-start' ? 2 : undefined} />
|
||
{children}
|
||
</Cell>
|
||
)
|
||
|
||
/** A choice in the form: its edge and its ground at rest and on approach. The
|
||
* chosen one holds its ground under the pointer. */
|
||
const CHOICE = {
|
||
borderColor: 'var(--border)',
|
||
bg: mix('var(--secondary)', 30),
|
||
hoverStyle: { bg: mix('var(--secondary)', 60) },
|
||
} as const
|
||
const CHOSEN = {
|
||
borderColor: mix('var(--pure-white)', 40),
|
||
bg: mix('var(--primary)', 10),
|
||
hoverStyle: { bg: mix('var(--primary)', 10) },
|
||
} as const
|
||
|
||
/** The box every field here is drawn in. `unstyled` zeroes the outline, so
|
||
* the focus ring @hanzo/design draws on every control is given its width back. */
|
||
const ENTRY = {
|
||
unstyled: true,
|
||
width: '100%',
|
||
px: 16,
|
||
py: 12,
|
||
rounded: 'var(--radius-lg)',
|
||
borderWidth: 1,
|
||
borderColor: 'var(--border)',
|
||
bg: 'var(--secondary)',
|
||
fontSize: '$2',
|
||
lineHeight: '$2',
|
||
color: 'var(--foreground)',
|
||
placeholderTextColor: '$placeholderColor',
|
||
focusVisibleStyle: { outlineWidth: 2 },
|
||
} as const
|
||
|
||
/** The two controls a form is made of: a line, and the same field `rows` tall. */
|
||
const Field = (props: GetProps<typeof Input>) => <Input {...ENTRY} {...props} />
|
||
|
||
const Note = (props: GetProps<typeof Input>) => <Input render="textarea" {...ENTRY} {...props} />
|
||
|
||
/** The platform's own select in the fields' box. */
|
||
const Choose = (props: React.ComponentProps<typeof Picker>) => (
|
||
<Picker
|
||
pl={16}
|
||
pr={16}
|
||
py={12}
|
||
minH={0}
|
||
rounded="var(--radius-lg)"
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
bg="var(--secondary)"
|
||
fontSize="$2"
|
||
$platform-web={{ lineHeight: '1.4286' }}
|
||
color="var(--foreground)"
|
||
{...props}
|
||
/>
|
||
)
|
||
|
||
const STAGES = [
|
||
{ id: 'idea', label: phrase('Idea') },
|
||
{ id: 'pre-seed', label: phrase('Pre-seed') },
|
||
{ id: 'seed', label: phrase('Seed') },
|
||
{ id: 'series-a', label: phrase('Series A') },
|
||
{ id: 'series-b-plus', label: phrase('Series B+') },
|
||
]
|
||
|
||
// Tier-1 funds recognized for the credits program. Free-text field captures any others.
|
||
const TIER1_FUNDS = [
|
||
'Andreessen Horowitz (a16z)',
|
||
'Sequoia',
|
||
'Benchmark',
|
||
'Greylock',
|
||
'Accel',
|
||
'Founders Fund',
|
||
'Lightspeed',
|
||
'Index Ventures',
|
||
'Bessemer',
|
||
'Khosla Ventures',
|
||
'GV (Google Ventures)',
|
||
'Techstars',
|
||
]
|
||
|
||
const USE_CASES = [
|
||
{ id: 'ai', label: phrase('AI & inference'), description: phrase('Models, agents, RAG, one API for every model') },
|
||
{ id: 'deploy', label: phrase('Deploy'), description: phrase('Apps, functions, edge, machines') },
|
||
{ id: 'data', label: phrase('Data'), description: phrase('Vector, SQL, KV, object storage') },
|
||
{ id: 'compute', label: phrase('Compute'), description: phrase('GPUs, training, batch jobs') },
|
||
]
|
||
|
||
// Each option is also the value filed with the application, so it stays English there.
|
||
const HEARD_OPTIONS = [
|
||
phrase('Search'),
|
||
'X / Twitter',
|
||
'LinkedIn',
|
||
phrase('A friend or colleague'),
|
||
phrase('A Techstars connection'),
|
||
phrase('An investor'),
|
||
phrase('An event or conference'),
|
||
phrase('Hanzo docs or GitHub'),
|
||
phrase('Other'),
|
||
]
|
||
|
||
const PILLARS = [
|
||
{
|
||
icon: Coins,
|
||
title: phrase('Start in minutes'),
|
||
body: phrase('Creating an account is free. Add funds when you are ready and pay only for what you use — no subscription, no minimums.'),
|
||
},
|
||
{
|
||
icon: Cpu,
|
||
title: phrase('Bring your own compute'),
|
||
body: phrase('Connect your own GPUs and machines. Run cheap on hardware you already have — we meter the platform, not your iron.'),
|
||
},
|
||
{
|
||
icon: Gift,
|
||
title: phrase('Up to $150,000 in credits'),
|
||
body: phrase('For venture-backed startups. Application required, and tier-1 VC backing is required for the credits program.'),
|
||
},
|
||
] as const
|
||
|
||
interface FormData {
|
||
company: string
|
||
website: string
|
||
contactName: string
|
||
email: string
|
||
role: string
|
||
stage: string
|
||
investors: string
|
||
tier1Investors: string[]
|
||
amountRaised: string
|
||
teamSize: string
|
||
building: string
|
||
useCases: string[]
|
||
infraSpend: string
|
||
byoCompute: boolean
|
||
byoHardware: string
|
||
techstarsBatch: string
|
||
heardVia: string
|
||
// Honeypot: must stay empty. Real users never see or fill this.
|
||
hp: string
|
||
}
|
||
|
||
const INITIAL_FORM: FormData = {
|
||
company: '',
|
||
website: '',
|
||
contactName: '',
|
||
email: '',
|
||
role: '',
|
||
stage: '',
|
||
investors: '',
|
||
tier1Investors: [],
|
||
amountRaised: '',
|
||
teamSize: '',
|
||
building: '',
|
||
useCases: [],
|
||
infraSpend: '',
|
||
byoCompute: false,
|
||
byoHardware: '',
|
||
techstarsBatch: '',
|
||
heardVia: '',
|
||
hp: '',
|
||
}
|
||
|
||
export default function StartupsPage() {
|
||
const { t } = useI18n()
|
||
const [formData, setFormData] = useState<FormData>(INITIAL_FORM)
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
const [submitted, setSubmitted] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
const handleChange = (
|
||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||
) => {
|
||
setFormData({ ...formData, [e.target.name]: e.target.value })
|
||
}
|
||
|
||
const toggleInArray = (key: 'tier1Investors' | 'useCases', value: string) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
[key]: prev[key].includes(value)
|
||
? prev[key].filter((v) => v !== value)
|
||
: [...prev[key], value],
|
||
}))
|
||
}
|
||
|
||
const validate = (): string | null => {
|
||
if (!formData.company.trim()) return t('Company name is required.')
|
||
if (!formData.contactName.trim()) return t('Your name is required.')
|
||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) return t('A valid email is required.')
|
||
if (!formData.stage) return t('Please select your company stage.')
|
||
if (!formData.building.trim()) return t('Tell us what you are building.')
|
||
return null
|
||
}
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
const validationError = validate()
|
||
if (validationError) {
|
||
setError(validationError)
|
||
return
|
||
}
|
||
setIsSubmitting(true)
|
||
setError(null)
|
||
|
||
try {
|
||
const res = await fetch(apply(), {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ ...formData, submittedAt: new Date().toISOString() }),
|
||
})
|
||
if (!res.ok) {
|
||
throw new Error(t('Request failed ({status})', { status: res.status }))
|
||
}
|
||
setSubmitted(true)
|
||
} catch (err) {
|
||
setError(
|
||
err instanceof Error ? err.message : t('Something went wrong. Please try again.')
|
||
)
|
||
} finally {
|
||
setIsSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
|
||
<Box render="main">
|
||
<Fold
|
||
label="Techstars ’17 — built for founders"
|
||
mark={<Rocket width={12} height={12} color="currentColor" aria-hidden />}
|
||
title="The Hanzo Startup Program"
|
||
lede="Everyone starts free. Venture-backed teams get up to $150,000 in credits."
|
||
actions={
|
||
<>
|
||
<Action href="#apply" fill>
|
||
{t('Apply for credits')}
|
||
<ArrowRight width={16} height={16} data-i18n-mirror />
|
||
</Action>
|
||
<Action href="https://platform.hanzo.ai" borderColor="var(--border)">
|
||
{t('Start building free')}
|
||
</Action>
|
||
</>
|
||
}
|
||
preview={<Steps title="The program" icon={<Rocket size={12} color="currentColor" />} steps={PILLARS.map((p) => ({ title: p.title, body: p.body }))} />}
|
||
/>
|
||
|
||
<Box render="section" pb={32} px={16} $md={{ px: 32 }} $lg={{ px: 48 }}>
|
||
{/* Techstars note */}
|
||
<Box width="100%" maxW={1024} mx="auto" mt={16}>
|
||
<XStack
|
||
items="flex-start"
|
||
gap={12}
|
||
p={20}
|
||
rounded="var(--radius-xl)"
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
bg="$hover"
|
||
>
|
||
<View render={<Rocket width={16} height={16} color={mix('var(--foreground)', 70)} />} mt={2} shrink={0} />
|
||
<Line render="p" display="block" fontSize="$2" color={MUTED} lineHeight="var(--leading-relaxed)">
|
||
{t('{label} Hanzo is a Techstars company. We offer perks tailored to each portfolio brand — mention your batch when you apply and we will match it.', {
|
||
label: (
|
||
<Text color="var(--foreground)" fontWeight="500">
|
||
{t('Techstars founders:')}
|
||
</Text>
|
||
),
|
||
})}
|
||
</Line>
|
||
</XStack>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Application form */}
|
||
<Box render="section" id="apply" py={56} px={16} $md={{ px: 32 }} $lg={{ px: 48 }} $platform-web={{ scrollMarginTop: '96px' }}>
|
||
<Box width="100%" maxW={672} mx="auto">
|
||
<Box mb={40} $platform-web={{ textAlign: 'center' }}>
|
||
<Title quiet mb={12}>
|
||
{t('Apply to the program')}
|
||
</Title>
|
||
<Text render="p" display="block" color={MUTED}>
|
||
{t('We review applications weekly. The more you share, the faster we can match you to the right tier.')}
|
||
</Text>
|
||
</Box>
|
||
|
||
{submitted ? (
|
||
<Box
|
||
render={<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.4 }} />}
|
||
p={40}
|
||
rounded="var(--radius-xl)"
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
bg={mix('var(--primary)', 5)}
|
||
$platform-web={{ textAlign: 'center' }}
|
||
>
|
||
<XStack
|
||
items="center"
|
||
justify="center"
|
||
width={56}
|
||
height={56}
|
||
mx="auto"
|
||
mb={20}
|
||
rounded="var(--radius-full)"
|
||
bg="$raised"
|
||
>
|
||
<CheckCircle2 width={28} height={28} color={mix('var(--foreground)', 70)} />
|
||
</XStack>
|
||
<Title quiet mb={12}>
|
||
{t('Application received')}
|
||
</Title>
|
||
<Text render="p" display="block" mb={8} color={MUTED}>
|
||
{t('Thanks — we review applications weekly and will be in touch by email.')}
|
||
</Text>
|
||
<Line render="p" display="block" mb={32} {...SM} color={MUTED}>
|
||
{t('Want to start building today? Create an account and pay only for what you use — no subscription required.')}
|
||
</Line>
|
||
<XStack flexWrap="wrap" justify="center" gap={16}>
|
||
<Action href="https://platform.hanzo.ai" fill>
|
||
{t('Start free')}
|
||
<ArrowRight width={16} height={16} data-i18n-mirror />
|
||
</Action>
|
||
<Action href="/" borderColor="var(--border)">
|
||
{t('Back to Home')}
|
||
</Action>
|
||
</XStack>
|
||
</Box>
|
||
) : (
|
||
<YStack
|
||
render={<motion.form onSubmit={handleSubmit} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1 }} />}
|
||
gap={24}
|
||
>
|
||
{/* Company + website */}
|
||
<Grid columns={DUO} gap={16}>
|
||
<Box>
|
||
<Label htmlFor="company" icon={Building2}>
|
||
{t('Company name')}
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="company"
|
||
name="company"
|
||
value={formData.company}
|
||
onChange={handleChange}
|
||
required
|
||
placeholder="Acme AI"
|
||
/>
|
||
</Box>
|
||
<Box>
|
||
<Label htmlFor="website" icon={Globe}>
|
||
{t('Website')}
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="website"
|
||
name="website"
|
||
value={formData.website}
|
||
onChange={handleChange}
|
||
placeholder="https://acme.ai"
|
||
/>
|
||
</Box>
|
||
</Grid>
|
||
|
||
{/* Contact name + email */}
|
||
<Grid columns={DUO} gap={16}>
|
||
<Box>
|
||
<Label htmlFor="contactName" icon={User}>
|
||
{t('Your name')}
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="contactName"
|
||
name="contactName"
|
||
value={formData.contactName}
|
||
onChange={handleChange}
|
||
required
|
||
placeholder="Jane Smith"
|
||
/>
|
||
</Box>
|
||
<Box>
|
||
<Label htmlFor="email" icon={Mail}>
|
||
{t('Work email')}
|
||
</Label>
|
||
<Field
|
||
type="email"
|
||
id="email"
|
||
name="email"
|
||
value={formData.email}
|
||
onChange={handleChange}
|
||
required
|
||
placeholder="[email protected]"
|
||
/>
|
||
</Box>
|
||
</Grid>
|
||
|
||
{/* Role + stage */}
|
||
<Grid columns={DUO} gap={16}>
|
||
<Box>
|
||
<Label htmlFor="role" icon={Briefcase}>
|
||
{t('Your role')}
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="role"
|
||
name="role"
|
||
value={formData.role}
|
||
onChange={handleChange}
|
||
placeholder={t('Co-founder & CEO')}
|
||
/>
|
||
</Box>
|
||
<Box>
|
||
<Label htmlFor="stage" icon={Rocket}>
|
||
{t('Company stage')}
|
||
</Label>
|
||
<Choose
|
||
id="stage"
|
||
name="stage"
|
||
value={formData.stage}
|
||
onChange={handleChange}
|
||
required
|
||
>
|
||
<option value="" disabled>
|
||
{t('Select stage')}
|
||
</option>
|
||
{STAGES.map((s) => (
|
||
<option key={s.id} value={s.id}>
|
||
{t(s.label)}
|
||
</option>
|
||
))}
|
||
</Choose>
|
||
</Box>
|
||
</Grid>
|
||
|
||
{/* Investors free text */}
|
||
<Box>
|
||
<Label htmlFor="investors" icon={Banknote}>
|
||
{t('Who are your investors?')}{' '}
|
||
<Aside>{t('(funds, angels)')}</Aside>
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="investors"
|
||
name="investors"
|
||
value={formData.investors}
|
||
onChange={handleChange}
|
||
placeholder={t('e.g. Sequoia, plus a few angels')}
|
||
/>
|
||
</Box>
|
||
|
||
{/* Tier-1 checkboxes */}
|
||
<Box>
|
||
<Label icon={CheckCircle2}>
|
||
{t('Backed by any of these?')}{' '}
|
||
<Aside>
|
||
{t('(tier-1 backing qualifies you for the credits program)')}
|
||
</Aside>
|
||
</Label>
|
||
<Grid columns={DUO} gap={8}>
|
||
{TIER1_FUNDS.map((fund) => (
|
||
<Toggle
|
||
key={fund}
|
||
on={formData.tier1Investors.includes(fund)}
|
||
onClick={() => toggleInArray('tier1Investors', fund)}
|
||
align="center"
|
||
pad={10}
|
||
>
|
||
<Line {...SM} color="var(--foreground)">{fund}</Line>
|
||
</Toggle>
|
||
))}
|
||
</Grid>
|
||
</Box>
|
||
|
||
{/* Raised + team size */}
|
||
<Grid columns={DUO} gap={16}>
|
||
<Box>
|
||
<Label htmlFor="amountRaised" icon={Banknote}>
|
||
{t('Amount raised')}{' '}
|
||
<Aside>{t('(optional)')}</Aside>
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="amountRaised"
|
||
name="amountRaised"
|
||
value={formData.amountRaised}
|
||
onChange={handleChange}
|
||
placeholder="$2M"
|
||
/>
|
||
</Box>
|
||
<Box>
|
||
<Label htmlFor="teamSize" icon={Users}>
|
||
{t('Team size')}
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="teamSize"
|
||
name="teamSize"
|
||
value={formData.teamSize}
|
||
onChange={handleChange}
|
||
placeholder="5"
|
||
/>
|
||
</Box>
|
||
</Grid>
|
||
|
||
{/* Building */}
|
||
<Box>
|
||
<Label htmlFor="building" icon={Boxes}>
|
||
{t('What are you building?')}
|
||
</Label>
|
||
<Note
|
||
id="building"
|
||
name="building"
|
||
value={formData.building}
|
||
onChange={handleChange}
|
||
required
|
||
rows={4}
|
||
placeholder={t('Describe your product, who it is for, and where you are today...')}
|
||
/>
|
||
</Box>
|
||
|
||
{/* Use cases */}
|
||
<Box>
|
||
<Label icon={Cpu}>
|
||
{t('How will you use Hanzo?')}
|
||
</Label>
|
||
<Grid columns={DUO} gap={8}>
|
||
{USE_CASES.map((uc) => (
|
||
<Toggle
|
||
key={uc.id}
|
||
on={formData.useCases.includes(uc.id)}
|
||
onClick={() => toggleInArray('useCases', uc.id)}
|
||
align="flex-start"
|
||
pad={12}
|
||
>
|
||
<span>
|
||
<Line {...SM} fontWeight="500" color="var(--foreground)">{t(uc.label)}</Line>
|
||
<Line render="p" display="block" mt={2} {...XS} color={MUTED}>{t(uc.description)}</Line>
|
||
</span>
|
||
</Toggle>
|
||
))}
|
||
</Grid>
|
||
</Box>
|
||
|
||
{/* Infra spend */}
|
||
<Box>
|
||
<Label htmlFor="infraSpend" icon={Banknote}>
|
||
{t('Current infra / compute spend per month')}{' '}
|
||
<Aside>{t('(optional)')}</Aside>
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="infraSpend"
|
||
name="infraSpend"
|
||
value={formData.infraSpend}
|
||
onChange={handleChange}
|
||
placeholder="$3,000 / mo"
|
||
/>
|
||
</Box>
|
||
|
||
{/* BYO compute */}
|
||
<YStack
|
||
gap={16}
|
||
p={20}
|
||
rounded="var(--radius-xl)"
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
bg="$panel"
|
||
>
|
||
<XStack
|
||
render={<button type="button" onClick={() => setFormData((p) => ({ ...p, byoCompute: !p.byoCompute }))} />}
|
||
items="flex-start"
|
||
gap={12}
|
||
width="100%"
|
||
$platform-web={{ textAlign: 'left' }}
|
||
>
|
||
<Check on={formData.byoCompute} size={20} mt={2} />
|
||
<span>
|
||
<Line display="flex" items="center" gap={8} {...SM} fontWeight="500" color="var(--foreground)">
|
||
<Cpu width={14} height={14} color={MUTED} />
|
||
{t('I want to bring my own compute')}
|
||
</Line>
|
||
<Line display="block" mt={4} {...XS} color={MUTED}>
|
||
{t('Connect your own GPUs or machines and run on hardware you already own.')}
|
||
</Line>
|
||
</span>
|
||
</XStack>
|
||
{formData.byoCompute && (
|
||
<Box>
|
||
<Line render="label" htmlFor="byoHardware" display="block" mb={8} {...XS} fontWeight="500" color={MUTED}>
|
||
{t('What hardware do you have?')}
|
||
</Line>
|
||
<Field
|
||
type="text"
|
||
id="byoHardware"
|
||
name="byoHardware"
|
||
value={formData.byoHardware}
|
||
onChange={handleChange}
|
||
placeholder={t('e.g. 8x H100, a few Mac Studios, on-prem cluster')}
|
||
/>
|
||
</Box>
|
||
)}
|
||
</YStack>
|
||
|
||
{/* Techstars + heard */}
|
||
<Grid columns={DUO} gap={16}>
|
||
<Box>
|
||
<Label htmlFor="techstarsBatch" icon={Rocket}>
|
||
{t('Techstars affiliation')}{' '}
|
||
<Aside>{t('(optional)')}</Aside>
|
||
</Label>
|
||
<Field
|
||
type="text"
|
||
id="techstarsBatch"
|
||
name="techstarsBatch"
|
||
value={formData.techstarsBatch}
|
||
onChange={handleChange}
|
||
placeholder={t('Batch / program, e.g. NYC W17')}
|
||
/>
|
||
</Box>
|
||
<Box>
|
||
<Label htmlFor="heardVia" icon={Globe}>
|
||
{t('How did you hear about us?')}
|
||
</Label>
|
||
<Choose
|
||
id="heardVia"
|
||
name="heardVia"
|
||
value={formData.heardVia}
|
||
onChange={handleChange}
|
||
>
|
||
<option value="">{t('Select one')}</option>
|
||
{HEARD_OPTIONS.map((h) => (
|
||
<option key={h} value={h}>
|
||
{t(h)}
|
||
</option>
|
||
))}
|
||
</Choose>
|
||
</Box>
|
||
</Grid>
|
||
|
||
{/* Honeypot: visually hidden, off-screen, not tab-reachable. */}
|
||
<Box aria-hidden="true" position="absolute" l={-9999} t={-9999} width={0} height={0} overflow="hidden">
|
||
<label htmlFor="hp">Do not fill this in</label>
|
||
<input
|
||
type="text"
|
||
id="hp"
|
||
name="hp"
|
||
value={formData.hp}
|
||
onChange={handleChange}
|
||
tabIndex={-1}
|
||
autoComplete="off"
|
||
/>
|
||
</Box>
|
||
|
||
{/* Error */}
|
||
{error && (
|
||
<Line
|
||
render="div"
|
||
display="block"
|
||
p={16}
|
||
rounded="var(--radius-lg)"
|
||
borderWidth={1}
|
||
fontSize="$2"
|
||
color="var(--state-error-text)"
|
||
lineHeight="1.4286" borderColor={mix('var(--state-error)', 30)}
|
||
bg={mix('var(--state-error)', 10)}
|
||
>
|
||
{error}
|
||
</Line>
|
||
)}
|
||
|
||
{/* Submit.
|
||
`alignItems` is what `w-full sm:w-auto` was asking for: the
|
||
control stretches on a phone and shrinks to its words from
|
||
640, which is a property of the COLUMN and not of the button. */}
|
||
<YStack pt={8} items="stretch" $sm={{ items: 'flex-start' }}>
|
||
<Action
|
||
render="button"
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
fill
|
||
justify="center"
|
||
opacity={isSubmitting ? 0.5 : undefined}
|
||
>
|
||
{isSubmitting ? (
|
||
t('Submitting...')
|
||
) : (
|
||
<>
|
||
<Send width={16} height={16} />
|
||
{t('Submit application')}
|
||
</>
|
||
)}
|
||
</Action>
|
||
</YStack>
|
||
|
||
<Line render="p" display="block" fontSize="$1" lineHeight="1.3333" color={mix('var(--muted-foreground)', 60)}>
|
||
{/* Nothing of its own: @hanzo/design's base underlines a link
|
||
inside a paragraph on approach, which is this site's whole
|
||
treatment for a link in a sentence. */}
|
||
{t('By submitting, you agree to Hanzo’s {link}.', {
|
||
link: <Link href="/legal/privacy">{t('Privacy Policy')}</Link>,
|
||
})}{' '}
|
||
{t('We use your information only to review this application.')}
|
||
</Line>
|
||
</YStack>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Fallback CTA for everyone */}
|
||
<Box render="section" pb={96} px={16} $md={{ px: 32 }} $lg={{ px: 48 }}>
|
||
<Box
|
||
width="100%"
|
||
maxW={768}
|
||
mx="auto"
|
||
p={32}
|
||
$sm={{ p: 40 }}
|
||
rounded="var(--radius-2xl)"
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
bg="$panel"
|
||
$platform-web={{ textAlign: 'center' }}
|
||
>
|
||
<Title quiet mb={12}>
|
||
{t('Not venture-backed yet?')}
|
||
</Title>
|
||
<Text render="p" display="block" mb={24} width="100%" maxW={576} mx="auto" color={MUTED}>
|
||
{t('You do not need to qualify to build. Create an account, pay only for what you use, and apply for credits when you raise.')}
|
||
</Text>
|
||
<Action href="https://platform.hanzo.ai" fill>
|
||
{t('Start free')}
|
||
<ArrowRight width={16} height={16} data-i18n-mirror />
|
||
</Action>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
)
|
||
}
|