Files
zeekay 4fd349aa06 beta: the portal says less, and opting in and sharing are one press
Prose that carried no decision or fact is gone: the hero, the questions, the
gate and every step are a heading and at most a line. What scores is said once
(the leaderboard step), and every claim the cloud does not keep is out.

Step 1 is one share card (the member's link, Copy, the device's share sheet on
phones, seven channels as icons, Customize behind a text button) and a quiet
invite-by-email card. Step 3 is one searched, grouped list with one control a
row: On when `opens` says so, Get access when it does not, Requested at once
after the press. Stars and follows moved under the leaderboard as Earn more.

The 1,800-line dashboard is a shell over share, rank, betas and alpha; what
they read is one hook (standing.ts); the questions are data (program.ts).
Dead access, card, hint and referral components are deleted. Every press is
44px, every input too, and a gate walks each screen at 390px.

scripts/brands and scripts/prebuild are TypeScript now; the three marks the
share row draws ship in a JSON only /beta loads. /beta's title and card say
what the page says.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 03:29:13 -07:00

133 lines
5.3 KiB
TypeScript

'use client'
import { useState } from 'react'
import { Check } from 'lucide-react'
import { Input, Text, Textarea, XStack, YStack, toast } from '@hanzo/ui'
import { Action, Title } from '@hanzo/ui/marketing'
import { phrase } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
import { track } from '@hanzo/app/lib/tags'
import { api } from '@hanzo/app/lib/api'
import { scope } from '@hanzo/app/lib/session'
import { Card } from './card'
import { Feedback } from './feedback'
import type { Program } from './program'
/** The three questions Alpha asks, and the id each answer is filed under. */
const ASKS = [
{ id: 'early_adopter', q: phrase('What have you been an early adopter of?'), long: false },
{ id: 'alpha_build', q: phrase('What will you build first?'), long: true },
{ id: 'why_alpha', q: phrase('Why you?'), long: true },
] as const
/**
* Step 4: Alpha, which is asked for with three answers, then a line to the team,
* and the two rooms the team is in. An application is a lead the team reads and
* the same answers on the member's record.
*/
export function Alpha({ email, name, program, asked, request }: { email: string; name: string; program: Program | null; asked: string[]; request: (ids: string[]) => Promise<void> }) {
const { t } = useI18n()
const [said, setSaid] = useState<Record<string, string>>({})
const [open, setOpen] = useState(false)
const [busy, setBusy] = useState(false)
const [slack, setSlack] = useState(false)
const applied = asked.includes('alpha')
const complete = ASKS.every((a) => (said[a.id] ?? '').trim())
const apply = async (e: React.FormEvent) => {
e.preventDefault()
if (!complete) return toast.error(t('Answer all three.'))
setBusy(true)
track('beta_alpha_questionnaire_submitted', { email })
const answers = ASKS.map((a) => ({ id: a.id, question: a.q, answer: [said[a.id].trim()] }))
const headers = { 'Content-Type': 'application/json', ...scope() }
try {
await Promise.all([
fetch(`${api()}/v1/marketing/leads`, {
method: 'POST',
headers,
body: JSON.stringify({
email,
name,
source: 'hanzo.ai/beta/alpha',
program: 'alpha',
need: answers.map((a) => `${a.question} ${a.answer[0]}`).join('\n\n'),
answers: answers.map((a) => ({ question: a.question, answer: a.answer[0] })),
}),
}).catch(() => {}),
fetch(`${api()}/v1/marketing/beta`, { method: 'PUT', headers, body: JSON.stringify({ program: 'alpha', answers }) }).catch(() => {}),
request(['alpha']),
])
setOpen(false)
toast.success(t('Alpha requested.'))
} finally {
setBusy(false)
}
}
const join = async () => {
track('beta_slack_requested', { email })
setSlack(true)
if (email) {
void fetch(`${api()}/v1/marketing/leads`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...scope() },
body: JSON.stringify({ email, name, source: 'hanzo.ai/beta/slack', program: 'developer_slack', need: 'Slack invite, from the beta portal' }),
}).catch(() => {})
}
await request(['slack'])
}
return (
<YStack gap={16}>
<Card data-slot="alpha-access">
<XStack items="center" justify="space-between" gap={12}>
<Title quiet>{t('Hanzo Alpha')}</Title>
{applied ? <Text fontFamily="$mono" fontSize="$2" color="var(--muted-foreground)">{t('Requested')}</Text> : null}
</XStack>
{applied && !open ? (
<XStack>
<Action render="button" type="button" onClick={() => setOpen(true)}>
{t('Update answers')}
</Action>
</XStack>
) : (
<form onSubmit={apply}>
<YStack gap={16}>
{ASKS.map((a) => (
<YStack key={a.id} gap={6}>
<Text fontSize="$3" fontWeight="600" color="var(--foreground)">
{t(a.q)}
</Text>
{a.long ? (
<Textarea rows={3} value={said[a.id] ?? ''} onChangeText={(v: string) => setSaid((s) => ({ ...s, [a.id]: v }))} aria-label={t(a.q)} />
) : (
<Input height={44} value={said[a.id] ?? ''} onChange={(e) => setSaid((s) => ({ ...s, [a.id]: e.target.value }))} aria-label={t(a.q)} />
)}
</YStack>
))}
<XStack>
<Action render="button" type="submit" fill disabled={busy || !complete}>
{busy ? t('Sending…') : t('Apply')}
</Action>
</XStack>
</YStack>
</form>
)}
</Card>
<Feedback program={program} email={email} name={name} />
<XStack gap={8} flexWrap="wrap">
<Action href="https://discord.gg/XthHQQj" target="_blank" rel="noopener noreferrer" onClick={() => track('beta_discord_clicked')}>
{t('Discord')}
</Action>
<Action render="button" type="button" disabled={slack || asked.includes('slack')} onClick={() => void join()}>
{slack || asked.includes('slack') ? <Check size={15} aria-hidden /> : null}
{slack || asked.includes('slack') ? t('Slack requested') : t('Join Slack')}
</Action>
</XStack>
</YStack>
)
}