mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
BOOKING is Hanzo AI's page at cal.com, where a visitor picks the 15- or 30-minute call. The sales form and "Book a 30-minute call" open INTRO, the 30-minute event page, which takes the name, email and notes the form fills in; the profile does not. A click to cal.com/hanzo is the sales_contacted moment, as cal.hanzo.ai was. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
509 lines
21 KiB
TypeScript
509 lines
21 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* /contact-sales — the one sales page: why Enterprise on the left, one short
|
||
* form on the right.
|
||
*
|
||
* Five questions: name, work email, company, team size and what they want to
|
||
* build. Submitting files a lead in Hanzo's own CRM (`POST /v1/marketing/leads`,
|
||
* cloud apps/marketing/leads.go), which states the lead on the event plane under
|
||
* the lead's own id, carrying the visit (lib/analytics/tags.ts `visit`: consent,
|
||
* the ad click, the browser ids) so cloud forwards it to Google and Meta. The
|
||
* page mirrors the same moment to the ad tags under that id (`mirror`) and says,
|
||
* in the card, that the request is in, with a call to book at cal.com/hanzo
|
||
* filled in with their details and the lead's reference.
|
||
*
|
||
* The lead also carries the campaign the visit arrived with (the utm_* tags,
|
||
* lib/campaign `brought`) and the offer code the page holds (lib/coupon), so
|
||
* sales sees where it came from.
|
||
*
|
||
* A lead that cannot be filed (cloud answers 503 until the brand org installs
|
||
* the CRM) still counts (`track`) and still reaches a person: the card offers
|
||
* the intro call and an email, both carrying the answers.
|
||
*
|
||
* Spam: the hidden `website` field, read off the form at submit. Whatever
|
||
* fills it is shown the same thanks a person sees, nothing is filed, and it is
|
||
* counted as `sales_trapped`. Per-client rate limits are the ingress's
|
||
* (leads.go). The submit is disabled until the page runs, so the export's form
|
||
* never sends the answers as an address.
|
||
*
|
||
* Two columns from `$lg`; below that the heading, then the form, then the
|
||
* rest, because the form is why the visitor came.
|
||
*/
|
||
|
||
import { useEffect, useState, useSyncExternalStore, type FormEvent, type ReactNode } from 'react'
|
||
import { HanzoMark } from '@hanzogui/shell'
|
||
import { Input, Label, Picker, Text, Textarea, View, XStack, YStack } from '@hanzo/ui'
|
||
import { ArrowRight, Calendar, Check, ChevronDown, CircleCheck } from 'lucide-react'
|
||
import { INTRO } from '@/components/home/nav-data'
|
||
import { TrustedMarquee } from '@/components/home/Trusted'
|
||
import { MAILBOX, PILL } from '@/components/sales/kit'
|
||
import { brought } from '@/lib/campaign'
|
||
import { clean, kept } from '@/lib/coupon'
|
||
import { mirror, track, visit } from '@hanzo/app/lib/tags'
|
||
import { api } from '@hanzo/app/lib/api'
|
||
import { START } from '@hanzo/app/lib/host'
|
||
import { Action, Display, Lede, More, TextLink } from '@hanzo/ui/marketing'
|
||
|
||
/** What Enterprise includes, in the plan's own terms (GET /v1/billing/plans, `enterprise`). */
|
||
const INCLUDES = [
|
||
'Run Hanzo in your cloud, on your servers or fully offline',
|
||
'Dedicated compute, and your own models trained and hosted',
|
||
'Unlimited members, single sign-on, roles and an audit log',
|
||
'An uptime guarantee and security review in your contract',
|
||
'24/7 engineering support and hands-on onboarding',
|
||
] as const
|
||
|
||
/** The headcount bands, as the CRM's lead records them (apps/crm Sizes). */
|
||
const SIZES: [string, string][] = [
|
||
['1-49', '1–49 people'],
|
||
['50-249', '50–249 people'],
|
||
['250-999', '250–999 people'],
|
||
['1000-4999', '1,000–4,999 people'],
|
||
['5000+', '5,000+ people'],
|
||
]
|
||
|
||
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||
|
||
interface Answers {
|
||
name: string
|
||
email: string
|
||
company: string
|
||
size: string
|
||
need: string
|
||
}
|
||
|
||
type Asked = Exclude<keyof Answers, 'need'>
|
||
|
||
/** What each required answer says when it is missing or malformed. */
|
||
const ASK: Record<Asked, string> = {
|
||
name: 'Enter your name.',
|
||
email: 'Enter a work email, like [email protected].',
|
||
company: 'Enter your company.',
|
||
size: 'Choose a team size.',
|
||
}
|
||
|
||
/** The answers that do not hold, in the order the form asks them. */
|
||
function check(a: Answers): Partial<Record<Asked, string>> {
|
||
const out: Partial<Record<Asked, string>> = {}
|
||
if (!a.name.trim()) out.name = ASK.name
|
||
if (!EMAIL.test(a.email.trim())) out.email = ASK.email
|
||
if (!a.company.trim()) out.company = ASK.company
|
||
if (!a.size) out.size = ASK.size
|
||
return out
|
||
}
|
||
|
||
/** The answers as one line, for a booking's notes and an email's body. */
|
||
const said = (a: Answers, lead?: string) =>
|
||
[a.company, a.size && `${a.size} people`, a.need, lead && `Lead ${lead}`].filter(Boolean).join(' · ')
|
||
|
||
/**
|
||
* Where the visit came from, as the lead records it: the utm_* tags of the link it
|
||
* arrived on and the offer code the page holds. A key with nothing to say is left
|
||
* out of the body.
|
||
*/
|
||
function arrived(): { campaign?: Record<string, string>; coupon?: string } {
|
||
const tags = brought(window.location.search).filter(([k]) => k.startsWith('utm_'))
|
||
const coupon = clean(new URLSearchParams(window.location.search).get('coupon')) ?? kept()
|
||
return { ...(tags.length ? { campaign: Object.fromEntries(tags) } : {}), ...(coupon ? { coupon } : {}) }
|
||
}
|
||
|
||
/** The calendar, opened on the intro call with the visitor's details in it. */
|
||
const booking = (a: Answers, lead?: string) => `${INTRO}?${new URLSearchParams({ name: a.name, email: a.email.trim(), notes: said(a, lead) })}`
|
||
|
||
/** An email to sales, written for the visitor from their answers. */
|
||
const letter = (a: Answers) =>
|
||
`mailto:${MAILBOX}?${new URLSearchParams({
|
||
subject: `Talk to sales: ${a.company}`,
|
||
body: `${a.name}\n${a.email.trim()}\n${a.company}\n${a.size ? `${a.size} people\n` : ''}\n${a.need}`,
|
||
})
|
||
.toString()
|
||
.replace(/\+/g, '%20')}`
|
||
|
||
/** The funnel's field (components/auth/page `FIELD`): 56px, 1rem type, 12px corners, on `--muted`. */
|
||
const FIELD = { size: 'lg', height: 56, fontSize: '1rem', rounded: 12, bg: 'var(--muted)' } as const
|
||
/**
|
||
* The native select at the same box: 56 high, its 24px line in the middle, and
|
||
* the platform's own drawing off, because WebKit draws a menu list at its own
|
||
* 26px whatever the padding says. The chevron is drawn beside it (`Choice`).
|
||
*/
|
||
const CHOICE = {
|
||
size: 'lg',
|
||
height: 56,
|
||
minH: 56,
|
||
py: 15,
|
||
pl: 14,
|
||
pr: 44,
|
||
fontSize: '1rem',
|
||
lineHeight: '1.5rem',
|
||
rounded: 12,
|
||
bg: 'var(--muted)',
|
||
'$platform-web': { appearance: 'none', WebkitAppearance: 'none' },
|
||
} as const
|
||
|
||
/** A store that never changes: `ready` reads only which side of hydration it is on. */
|
||
const still = () => () => {}
|
||
|
||
/** A field said wrong wears the error edge until it is mended, under the pointer and in focus too. */
|
||
const BAD = {
|
||
borderColor: 'var(--state-error)',
|
||
hoverStyle: { borderColor: 'var(--state-error)' },
|
||
focusStyle: { borderColor: 'var(--state-error)' },
|
||
} as const
|
||
|
||
/** The answers a page carried in its address: what a form sent before this page ran, which the address must not keep. */
|
||
const ASKED = ['name', 'email', 'company', 'size', 'need', 'website']
|
||
|
||
type State = { at: 'form' } | { at: 'sent'; lead?: string } | { at: 'held' }
|
||
|
||
export default function ContactSales() {
|
||
const [answers, setAnswers] = useState<Answers>({ name: '', email: '', company: '', size: '', need: '' })
|
||
const [wrong, setWrong] = useState<Partial<Record<Asked, string>>>({})
|
||
const [trap, setTrap] = useState('')
|
||
const [busy, setBusy] = useState(false)
|
||
const [state, setState] = useState<State>({ at: 'form' })
|
||
// The export's form is inert until this page runs: a press before then would
|
||
// send the answers as a plain GET, into the address, and file nothing. False
|
||
// in the export and through hydration, true once the page runs.
|
||
const ready = useSyncExternalStore(still, () => true, () => false)
|
||
useEffect(() => {
|
||
const q = new URLSearchParams(window.location.search)
|
||
if (!ASKED.some((k) => q.has(k))) return
|
||
for (const k of ASKED) q.delete(k)
|
||
const rest = q.toString()
|
||
window.history.replaceState(null, '', `${window.location.pathname}${rest ? `?${rest}` : ''}${window.location.hash}`)
|
||
}, [])
|
||
|
||
const set = (k: keyof Answers) => (v: string) => {
|
||
setAnswers((a) => ({ ...a, [k]: v }))
|
||
// A field said wrong is cleared as it is mended, never re-judged mid-word.
|
||
if (k !== 'need' && wrong[k]) setWrong((w) => ({ ...w, [k]: undefined }))
|
||
}
|
||
|
||
/** Once the card says what happened, it holds the focus, so a screen reader reads it. */
|
||
const show = (next: State) => {
|
||
setState(next)
|
||
requestAnimationFrame(() => document.getElementById('sales-card')?.focus())
|
||
}
|
||
|
||
const send = async (e: FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault()
|
||
if (busy) return
|
||
// Read off the form itself, so a value set without an input event counts.
|
||
const trapped = Boolean(new FormData(e.currentTarget).get('website') || trap)
|
||
const missing = check(answers)
|
||
setWrong(missing)
|
||
const first = (Object.keys(missing) as Asked[])[0]
|
||
if (first) {
|
||
document.getElementById(`sales-${first}`)?.focus()
|
||
return
|
||
}
|
||
// A form a person cannot see was filled in by something else: it gets the
|
||
// thanks a person gets, nothing is filed, and the moment is counted.
|
||
if (trapped) {
|
||
track('sales_trapped', { lead_source: 'contact-sales' })
|
||
return show({ at: 'sent' })
|
||
}
|
||
setBusy(true)
|
||
const seen = visit()
|
||
const moment = { lead_source: 'contact-sales', company_size: answers.size }
|
||
let lead: string | undefined
|
||
try {
|
||
const r = await fetch(`${api()}/v1/marketing/leads`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ ...answers, email: answers.email.trim(), source: 'hanzo.ai/contact-sales', visit: seen, ...arrived() }),
|
||
})
|
||
if (r.ok) lead = ((await r.json()) as { lead?: string }).lead
|
||
} catch {
|
||
lead = undefined
|
||
}
|
||
setBusy(false)
|
||
if (!lead) {
|
||
track('sales_contacted', moment)
|
||
return show({ at: 'held' })
|
||
}
|
||
// The CRM stated it; the ad tags hear it under the lead's id, and our
|
||
// stream is not told twice.
|
||
mirror('sales_contacted', { ...moment, lead_id: lead }, lead, seen.tags as string)
|
||
show({ at: 'sent', lead })
|
||
}
|
||
|
||
return (
|
||
<YStack
|
||
render="section"
|
||
aria-label="Talk to sales"
|
||
position="relative"
|
||
overflow="hidden"
|
||
pt={112}
|
||
pb={96}
|
||
px="$4"
|
||
$md={{ px: '$6' }}
|
||
$lg={{ pt: 136, pb: 128, px: '$8' }}
|
||
>
|
||
{/* The light the page opens under, from the top right. */}
|
||
<View
|
||
aria-hidden
|
||
position="absolute"
|
||
t={0}
|
||
l={0}
|
||
r={0}
|
||
height={560}
|
||
pointerEvents="none"
|
||
backgroundImage="radial-gradient(ellipse 60% 100% at 70% 0%, var(--white-08), transparent 70%)"
|
||
/>
|
||
<View
|
||
position="relative"
|
||
display="grid"
|
||
gap="$7"
|
||
width="100%"
|
||
maxW={1152}
|
||
mx="auto"
|
||
gridTemplateAreas={"'head' 'form' 'more'"}
|
||
$lg={{
|
||
gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)',
|
||
gridTemplateRows: 'auto 1fr',
|
||
gridTemplateAreas: "'head form' 'more form'",
|
||
columnGap: 96,
|
||
rowGap: 48,
|
||
items: 'flex-start',
|
||
}}
|
||
>
|
||
<YStack gridArea="head" minW={0} gap="$4.5">
|
||
<XStack render="p" items="center" gap="$2.5" mb="$4">
|
||
<HanzoMark size={20} />
|
||
<Text fontSize="$4" fontWeight="500" color="$ink">
|
||
Enterprise
|
||
</Text>
|
||
</XStack>
|
||
<Display>Talk to sales</Display>
|
||
<YStack maxW={460} gap="$2">
|
||
<Lede>Tell us what you want to build. We come back with a plan for your company: where Hanzo runs, what it costs and how you start.</Lede>
|
||
<Text fontSize="$2" color="var(--muted-foreground)">
|
||
Want instant recommendations or team seat pricing?{' '}
|
||
<TextLink href="/buying-specialist">Talk to AI Buying Specialist →</TextLink>
|
||
</Text>
|
||
</YStack>
|
||
</YStack>
|
||
|
||
<YStack gridArea="form" minW={0} gap="$4">
|
||
<YStack
|
||
id="sales-card"
|
||
tabIndex={-1}
|
||
data-slot="sales-card"
|
||
data-state={state.at}
|
||
aria-live="polite"
|
||
p="$5"
|
||
gap="$5"
|
||
bg="$panel"
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
rounded="var(--radius-2xl)"
|
||
boxShadow="var(--edge-highlight), var(--shadow-floating)"
|
||
$sm={{ p: 32 }}
|
||
$platform-web={{ outline: 'none' }}
|
||
>
|
||
{state.at === 'sent' ? (
|
||
<Sent answers={answers} lead={state.lead} />
|
||
) : state.at === 'held' ? (
|
||
<Held answers={answers} />
|
||
) : (
|
||
<form onSubmit={send} method="post" noValidate aria-label="Talk to sales">
|
||
<YStack gap="$4">
|
||
<Ask id="sales-name" label="Full name" wrong={wrong.name}>
|
||
<Input id="sales-name" {...FIELD} {...(wrong.name ? BAD : null)} name="name" autoComplete="name" value={answers.name} onChange={(e) => set('name')(e.target.value)} required aria-invalid={Boolean(wrong.name)} aria-describedby={wrong.name ? 'sales-name-wrong' : undefined} />
|
||
</Ask>
|
||
<Ask id="sales-email" label="Work email" wrong={wrong.email}>
|
||
<Input
|
||
id="sales-email"
|
||
{...FIELD}
|
||
{...(wrong.email ? BAD : null)}
|
||
type="email"
|
||
name="email"
|
||
autoComplete="email"
|
||
inputMode="email"
|
||
value={answers.email}
|
||
onChange={(e) => set('email')(e.target.value)}
|
||
onBlur={() => answers.email.trim() && !EMAIL.test(answers.email.trim()) && setWrong((w) => ({ ...w, email: ASK.email }))}
|
||
placeholder="[email protected]"
|
||
required
|
||
aria-invalid={Boolean(wrong.email)}
|
||
aria-describedby={wrong.email ? 'sales-email-wrong' : undefined}
|
||
/>
|
||
</Ask>
|
||
<View display="grid" gap="$4" $sm={{ gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)' }}>
|
||
<Ask id="sales-company" label="Company" wrong={wrong.company}>
|
||
<Input id="sales-company" {...FIELD} {...(wrong.company ? BAD : null)} name="company" autoComplete="organization" value={answers.company} onChange={(e) => set('company')(e.target.value)} required aria-invalid={Boolean(wrong.company)} aria-describedby={wrong.company ? 'sales-company-wrong' : undefined} />
|
||
</Ask>
|
||
<Ask id="sales-size" label="Team size" wrong={wrong.size}>
|
||
<Choice>
|
||
<Picker
|
||
id="sales-size"
|
||
{...CHOICE}
|
||
{...(wrong.size ? BAD : null)}
|
||
name="size"
|
||
value={answers.size}
|
||
onChange={(e) => set('size')(e.target.value)}
|
||
required
|
||
aria-invalid={Boolean(wrong.size)}
|
||
aria-describedby={wrong.size ? 'sales-size-wrong' : undefined}
|
||
color={answers.size ? '$ink' : '$placeholderColor'}
|
||
>
|
||
<option value="" disabled>
|
||
Choose
|
||
</option>
|
||
{SIZES.map(([v, label]) => (
|
||
// inline-style: an <option> is drawn by the platform and takes no gui prop; its ink is stated so the open list reads at full strength while the closed select shows the dim hint.
|
||
<option key={v} value={v} style={{ color: 'var(--foreground)' }}>
|
||
{label}
|
||
</option>
|
||
))}
|
||
</Picker>
|
||
</Choice>
|
||
</Ask>
|
||
</View>
|
||
<Ask id="sales-need" label="What do you want to build?">
|
||
<Textarea
|
||
id="sales-need"
|
||
size="lg"
|
||
fontSize="1rem"
|
||
rounded={12}
|
||
bg="var(--muted)"
|
||
name="need"
|
||
rows={4}
|
||
value={answers.need}
|
||
onChangeText={set('need')}
|
||
placeholder="Agents for our support team, Hanzo on our own servers…"
|
||
/>
|
||
</Ask>
|
||
{/* A field no person can see: whatever fills it is not a person. */}
|
||
<View render="label" aria-hidden position="absolute" l={-9999} width={1} height={1} overflow="hidden">
|
||
<span>Leave this empty</span>
|
||
<input tabIndex={-1} autoComplete="off" name="website" value={trap} onChange={(e) => setTrap(e.target.value)} />
|
||
</View>
|
||
<YStack gap="$3" mt="$2">
|
||
<Action render="button" type="submit" fill disabled={busy || !ready} {...PILL} width="100%">
|
||
{busy ? 'Sending…' : 'Talk to sales'}
|
||
</Action>
|
||
<XStack justify="center">
|
||
<More href={INTRO} target="_blank" rel="noopener noreferrer" minH={44}>
|
||
<Calendar width={16} height={16} aria-hidden />
|
||
Or book a 30-minute call
|
||
</More>
|
||
</XStack>
|
||
</YStack>
|
||
</YStack>
|
||
</form>
|
||
)}
|
||
</YStack>
|
||
<Text render="p" text="center" fontSize="$2" color="var(--muted-foreground)">
|
||
Prefer email? <TextLink href="mailto:[email protected]">enterprise@hanzo.ai</TextLink>. Want it today?{' '}
|
||
<TextLink href={START}>Get Hanzo Team</TextLink>.
|
||
</Text>
|
||
</YStack>
|
||
|
||
<YStack gridArea="more" minW={0} gap="$8">
|
||
<YStack gap="$4">
|
||
<Text render="h2" fontSize="$4" fontWeight="600" color="$ink">
|
||
What Enterprise includes
|
||
</Text>
|
||
<YStack render="ul" data-slot="sales-features" gap="$3" m={0} p={0} $platform-web={{ listStyle: 'none' }}>
|
||
{INCLUDES.map((says) => (
|
||
<XStack render="li" key={says} items="flex-start" gap="$2.5">
|
||
<View shrink={0} mt={3}>
|
||
<Check size={16} color="var(--foreground)" aria-hidden />
|
||
</View>
|
||
<Text fontSize="$3" lineHeight="1.5rem" color="$ink">
|
||
{says}
|
||
</Text>
|
||
</XStack>
|
||
))}
|
||
</YStack>
|
||
<More href="/enterprise" self="flex-start">
|
||
See everything in Enterprise
|
||
<ArrowRight width={14} height={14} aria-hidden />
|
||
</More>
|
||
</YStack>
|
||
<YStack gap="$4" minW={0} maxW="100%" overflow="hidden">
|
||
<Text render="p" fontSize="$2" color="var(--muted-foreground)">
|
||
Trusted by
|
||
</Text>
|
||
<TrustedMarquee height={26} />
|
||
</YStack>
|
||
</YStack>
|
||
</View>
|
||
</YStack>
|
||
)
|
||
}
|
||
|
||
/** A labelled field, and what is wrong with it under it. */
|
||
function Ask({ id, label, wrong, children }: { id: string; label: string; wrong?: string; children: ReactNode }) {
|
||
return (
|
||
<YStack gap="$2" minW={0}>
|
||
<Label htmlFor={id}>{label}</Label>
|
||
{children}
|
||
{wrong ? (
|
||
<Text render="p" id={`${id}-wrong`} fontSize="$2" color="var(--state-error-text)" $theme-light={{ color: 'var(--destructive-hover)' }}>
|
||
{wrong}
|
||
</Text>
|
||
) : null}
|
||
</YStack>
|
||
)
|
||
}
|
||
|
||
/** The select and its chevron, which the platform no longer draws. */
|
||
function Choice({ children }: { children: ReactNode }) {
|
||
return (
|
||
<View position="relative" minW={0}>
|
||
{children}
|
||
<View position="absolute" r={16} t={0} b={0} justify="center" pointerEvents="none" aria-hidden>
|
||
<ChevronDown size={18} color="var(--muted-foreground)" />
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
/** The request is in: who will hear back, and a call to book now if they want one. */
|
||
function Sent({ answers, lead }: { answers: Answers; lead?: string }) {
|
||
const first = answers.name.trim().split(/\s+/)[0]
|
||
return (
|
||
<YStack gap="$4" items="flex-start" data-slot="sales-sent">
|
||
<CircleCheck size={32} color="var(--foreground)" aria-hidden />
|
||
<Text render="h2" fontSize="$7" lineHeight="$8" fontWeight="500" color="$ink">
|
||
Thanks{first ? `, ${first}` : ''}. Your request is in.
|
||
</Text>
|
||
<Text render="p" fontSize="$3" lineHeight="1.5rem" color="var(--muted-foreground)">
|
||
Our team will reply to {answers.email.trim()}. Want to talk sooner? Pick a time for a 30-minute call.
|
||
</Text>
|
||
<Action href={booking(answers, lead)} target="_blank" rel="noopener noreferrer" fill {...PILL} width="100%" mt="$2">
|
||
<Calendar width={18} height={18} aria-hidden />
|
||
Book a call
|
||
</Action>
|
||
<More href="/enterprise" self="center">
|
||
See everything in Enterprise
|
||
</More>
|
||
</YStack>
|
||
)
|
||
}
|
||
|
||
/** The request is not filed yet: the call or an email carries the answers to a person. */
|
||
function Held({ answers }: { answers: Answers }) {
|
||
return (
|
||
<YStack gap="$4" items="flex-start" data-slot="sales-held">
|
||
<Calendar size={32} color="var(--foreground)" aria-hidden />
|
||
<Text render="h2" fontSize="$7" lineHeight="$8" fontWeight="500" color="$ink">
|
||
One more step
|
||
</Text>
|
||
<Text render="p" fontSize="$3" lineHeight="1.5rem" color="var(--muted-foreground)">
|
||
Pick a time for a 30-minute call. Your answers go with the booking.
|
||
</Text>
|
||
<Action href={booking(answers)} target="_blank" rel="noopener noreferrer" fill {...PILL} width="100%" mt="$2">
|
||
Pick a time
|
||
</Action>
|
||
<More href={letter(answers)} self="center">
|
||
Or email {MAILBOX} with your answers
|
||
</More>
|
||
</YStack>
|
||
)
|
||
}
|