mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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
133 lines
5.3 KiB
TypeScript
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>
|
|
)
|
|
}
|