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

179 lines
7.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useMemo, useState } from 'react'
import { ArrowRight } from 'lucide-react'
import { Text, View, XStack, YStack } from '@hanzo/ui'
import { Action, Chip, Title } from '@hanzo/ui/marketing'
import { BOOKING } from '@/components/home/nav-data'
import { useI18n } from '@hanzo/i18n/react'
import { useIam } from '@hanzo/iam/react'
import { track } from '@hanzo/app/lib/tags'
import { BETAS, opens } from '@hanzo/app/lib/betas'
import { scope } from '@hanzo/app/lib/session'
import { useTier } from '@hanzo/app/lib/tier'
import { Alpha } from './alpha'
import { Betas } from './betas'
import { Card, Quiet } from './card'
import { Rank } from './rank'
import { Share } from './share'
import { useStanding } from './standing'
import type { Program } from './program'
export interface BetaDashboardProps {
email: string
name?: string
program?: Program | null
paid?: boolean
checkoutUrl?: string
/** The live deal in words, from the quote: "50% off Pro for your first 3 months". */
offer?: string
onEditApplication?: () => void
}
/** The plan a member is on, as the header names it. */
function plan(raw: string, paid: boolean): string {
const r = raw.toLowerCase()
if (r.includes('agency')) return 'Agency'
if (r.includes('enterprise')) return 'Enterprise'
if (r.includes('team')) return 'Team'
if (r.includes('max') || r.includes('ultra')) return 'Max'
return r.includes('pro') || r.includes('dev') || paid ? 'Pro' : 'Free'
}
/**
* The member's beta, in four steps: share, the leaderboard, the betas, Alpha and
* a word to the team. What each reads is `useStanding`; what each says about
* scoring is said once, in the second.
*/
export function BetaDashboard({ email, name = '', program = 'beta', paid = false, checkoutUrl = '/pay/cart?coupon=50OFFBETA', offer, onEditApplication }: BetaDashboardProps) {
const { t } = useI18n()
const { isAuthenticated } = useIam()
const signed = Boolean(isAuthenticated && typeof window !== 'undefined' && scope().Authorization)
const { tier } = useTier(signed)
const s = useStanding(signed)
const [step, setStep] = useState<1 | 2 | 3 | 4>(1)
const handle = s.code ? `@${s.code}` : email ? `@${email.split('@')[0]}` : t('you')
const on = useMemo(() => BETAS.filter((b) => opens(b.id, s.flags, s.staff)).length, [s.flags, s.staff])
if (!signed) {
return (
<Card gap={20} maxW={680} mx="auto" data-slot="beta-account-gate">
<Title render="h1">{t('Create your account to continue.')}</Title>
<XStack gap={10} flexWrap="wrap">
<Action href={`/signup?next=${encodeURIComponent('/beta')}`} fill>
{t('Create account')}
<ArrowRight size={16} aria-hidden />
</Action>
<Action href={`/login?next=${encodeURIComponent('/beta')}`}>{t('Sign in')}</Action>
</XStack>
</Card>
)
}
const steps = [
{ n: 1, title: t('1. Share & Earn'), sub: t('{n} referred', { n: s.referred }) },
{ n: 2, title: t('2. Leaderboard'), sub: `${s.place > 0 ? `#${s.place}` : '—'} · ${s.total} ${t('pts')}` },
{ n: 3, title: t('3. Ecosystem Betas'), sub: t('{n} on', { n: on }) },
{ n: 4, title: t('4. Alpha & Feedback'), sub: s.asked.includes('alpha') ? t('Requested') : t('Apply') },
] as const
return (
<YStack data-motion="rise" gap={16} width="100%" maxW={960} mx="auto" data-slot="beta-dashboard">
<Card gap={20}>
<XStack items="flex-start" justify="space-between" flexWrap="wrap" gap={16}>
<YStack gap={4} flex={1} minW={0} flexBasis={280}>
<Title render="h1">{paid ? t('You’re in the Hanzo beta.') : t('Your beta application is in.')}</Title>
{email ? (
<Text fontSize="$3" color="var(--muted-foreground)" $platform-web={{ overflowWrap: 'anywhere' }}>
{email}
</Text>
) : null}
</YStack>
<XStack gap={8} flexWrap="wrap">
<Chip px={14} py={8}>
{s.total} {t('pts')}
</Chip>
<Chip px={14} py={8}>
{s.place > 0 ? `#${s.place}` : '—'}
{s.of > 0 ? ` / ${s.of.toLocaleString()}` : ''}
</Chip>
<Chip px={14} py={8}>
{t('{plan} plan', { plan: plan(tier?.tier?.name || tier?.plan || '', paid) })}
</Chip>
</XStack>
</XStack>
<XStack items="center" gap={10} flexWrap="wrap">
{paid ? (
<Action href="/chat" fill>
{t('Open app')}
</Action>
) : (
<>
<Action href={checkoutUrl} fill>
{t('Continue with plan')}
<ArrowRight size={15} aria-hidden data-i18n-mirror />
</Action>
<Action href="/pricing?coupon=50OFFBETA">{t('View plans')}</Action>
</>
)}
{onEditApplication ? (
<Quiet render="button" type="button" onClick={onEditApplication}>
{t('Review answers')}
</Quiet>
) : null}
{!paid && offer ? (
<Text fontSize="$2" color="var(--muted-foreground)">
{offer}
</Text>
) : null}
</XStack>
<View display="grid" gap={8} pt={20} borderTopWidth={1} borderColor="var(--border)" gridTemplateColumns="repeat(2, minmax(0, 1fr))" $sm={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}>
{steps.map((x) => (
<YStack
key={x.n}
render={
<button
type="button"
aria-pressed={step === x.n}
onClick={() => {
track('beta_dashboard_step_selected', { step: x.n })
setStep(x.n)
}}
/>
}
gap={2}
px={14}
py={10}
rounded={12}
borderWidth={1}
borderColor={step === x.n ? 'var(--foreground)' : 'var(--border)'}
bg={step === x.n ? 'var(--secondary)' : 'transparent'}
$platform-web={{ textAlign: 'start' }}
>
<Text fontSize="$3" fontWeight="600" color="var(--foreground)">
{x.title}
</Text>
<Text fontFamily="$mono" fontSize="$2" color="var(--muted-foreground)">
{x.sub}
</Text>
</YStack>
))}
</View>
</Card>
{step === 1 ? <Share link={s.link} code={s.code} program={program ?? 'beta'} rename={s.rename} /> : null}
{step === 2 ? <Rank s={s} handle={handle} /> : null}
{step === 3 ? <Betas flags={s.flags} staff={s.staff} asked={s.asked} request={s.request} /> : null}
{step === 4 ? <Alpha email={email} name={name} program={program} asked={s.asked} request={s.request} /> : null}
<XStack>
<Quiet href={BOOKING} target="_blank" rel="noopener noreferrer" onClick={() => track('beta_call_booked', { from: 'dashboard' })}>
{t('Book a call')}
</Quiet>
</XStack>
</YStack>
)
}