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

173 lines
7.6 KiB
TypeScript

'use client'
import { Check, Star } from 'lucide-react'
import { Input, Text, View, XStack, YStack, toast } from '@hanzo/ui'
import { Action, Chip, Title } from '@hanzo/ui/marketing'
import { useI18n } from '@hanzo/i18n/react'
import { track } from '@hanzo/app/lib/tags'
import { did, HANZO_FOUNDERS, HANZO_REPOS, HANZO_SOCIALS } from '@/lib/hanzo/beta'
import { Card } from './card'
import type { useStanding } from './standing'
/** What scores, said once. Shares count for nothing by themselves: a signup does. */
const RULES = [
{ pts: '+1', what: 'signup' },
{ pts: '+20', what: 'referral that pays' },
{ pts: '+1', what: '100K tokens' },
{ pts: '+10', what: 'repo star' },
{ pts: '+1', what: 'follow' },
] as const
interface Row {
rank: number
handle: string
tokens?: string
referrals: number
score: number
you: boolean
}
/**
* Step 2: the waitlist ranked by one score, who is on it, and the ways to earn
* more (a star is checked against GitHub, a follow is taken on trust). Until the
* cloud answers a board, only the member's own row is drawn.
*/
export function Rank({ s, handle }: { s: ReturnType<typeof useStanding>; handle: string }) {
const { t } = useI18n()
const tokens = (r: string) => (r && r !== '0' ? t('{range} tokens', { range: r }) : undefined)
const rows: Row[] = s.ranked
? s.ranked.rows.map((r) => ({ rank: r.rank, handle: r.name ? `@${r.name}` : r.you ? handle : t('A builder'), tokens: tokens(r.tokens), referrals: r.referrals, score: r.score, you: r.you }))
: [{ rank: s.place, handle, referrals: s.referred, score: s.total, you: true }]
// Whoever ranks below the top still sees their own row, after the top.
const me = s.ranked?.you
if (me && !rows.some((r) => r.you)) rows.push({ rank: me.rank, handle: me.name ? `@${me.name}` : handle, tokens: tokens(me.tokens), referrals: me.referrals, score: me.score, you: true })
const star = async (slug: string) => {
window.open(`https://github.com/${slug}`, '_blank', 'noopener,noreferrer')
track('beta_repo_star_clicked', { repo: slug })
const gh = s.github.trim().replace(/^@/, '')
if (!gh) return toast.message(t('Enter your GitHub username so the star can be checked.'))
const r = await s.earn('star', slug, gh)
if (!r.ok && r.error) toast.error(r.error)
}
const follow = (who: string, kind: string, href: string) => {
window.open(href, '_blank', 'noopener,noreferrer')
track('beta_founder_follow_clicked', { target: `${who}/${kind}` })
void s.earn('follow', `${who}/${kind}`)
}
return (
<YStack gap={16}>
<Card gap={20} data-slot="beta-leaderboard">
<XStack items="center" justify="space-between" flexWrap="wrap" gap={12}>
<Title quiet>{t('Leaderboard')}</Title>
{s.ranked ? (
<Text fontFamily="$mono" fontSize="$2" color="var(--muted-foreground)">
{t('{n} on the waitlist', { n: s.ranked.of.toLocaleString() })}
</Text>
) : null}
</XStack>
<View display="grid" gap={8} gridTemplateColumns="repeat(2, minmax(0, 1fr))" $md={{ gridTemplateColumns: 'repeat(5, minmax(0, 1fr))' }}>
{RULES.map((r) => (
<YStack key={r.what} gap={2} px={14} py={10} rounded={12} bg="var(--muted)">
<Text fontFamily="$mono" fontSize="$4" fontWeight="700" color="var(--foreground)">
{r.pts}
</Text>
<Text fontSize="$2" color="var(--muted-foreground)">
{t(r.what)}
</Text>
</YStack>
))}
</View>
<Text fontSize="$2" color="var(--muted-foreground)">
{t('Everyone who applied earlier stays ahead on a tie.')}
</Text>
<YStack borderWidth={1} borderColor="var(--border)" rounded={14} overflow="hidden">
{rows.map((r, i) => (
<XStack key={`${r.rank}-${r.handle}`} items="center" gap={12} px={16} py={12} borderTopWidth={i ? 1 : 0} borderColor="var(--border)" bg={r.you ? 'var(--secondary)' : 'transparent'}>
<Text fontFamily="$mono" fontSize="$3" fontWeight="700" color="var(--foreground)" minW={48}>
{r.rank > 0 ? `#${r.rank}` : '—'}
</Text>
<YStack flex={1} minW={0}>
<Text fontFamily="$mono" fontSize="$3" fontWeight="600" color="var(--foreground)" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
{r.handle}
{r.you ? ` · ${t('you')}` : ''}
</Text>
{r.tokens ? (
<Text fontSize="$2" color="var(--muted-foreground)">
{r.tokens}
</Text>
) : null}
</YStack>
<YStack items="flex-end">
<Text fontFamily="$mono" fontSize="$3" fontWeight="700" color="var(--foreground)">
{r.score.toLocaleString()}
</Text>
<Text fontSize="$2" color="var(--muted-foreground)">
{t('{n} referred', { n: r.referrals })}
</Text>
</YStack>
</XStack>
))}
</YStack>
{s.listed?.canSet ? (
<XStack>
<Action render="button" type="button" onClick={() => void s.show(!s.listed?.listed)}>
{s.listed.listed ? t('Take my name off the board') : t('Show my name on the board')}
</Action>
</XStack>
) : null}
{s.referees.length ? (
<XStack gap={8} flexWrap="wrap">
{s.referees.map((r) => (
<Chip key={r.id} px={12} py={6}>
{r.referee}
{r.status === 'qualified' ? ` · ${t('pays')}` : ''}
</Chip>
))}
</XStack>
) : null}
</Card>
<Card gap={20} bg="transparent" data-slot="beta-earn">
<Title quiet>{t('Earn more')}</Title>
<Input height={44} placeholder={t('GitHub username')} aria-label={t('GitHub username')} value={s.github} onChange={(e) => s.keep(e.target.value)} />
<YStack>
{HANZO_REPOS.map((repo, i) => {
const done = did(s.actions, 'star', repo.slug)
return (
<XStack key={repo.slug} items="center" gap={12} py={12} borderTopWidth={i ? 1 : 0} borderColor="var(--border)">
<Text flex={1} minW={0} fontFamily="$mono" fontSize="$3" color="var(--foreground)" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
{repo.label}
</Text>
<Action render="button" type="button" fill={done} onClick={() => void star(repo.slug)}>
{done ? <Check size={15} aria-hidden /> : <Star size={15} aria-hidden />}
{done ? t('Starred') : t('Star')}
</Action>
</XStack>
)
})}
</YStack>
<XStack gap={8} flexWrap="wrap">
{[{ who: 'hanzo', name: 'Hanzo', channels: HANZO_SOCIALS.map((c) => ({ kind: c.kind, label: c.label, href: c.href })) }, ...HANZO_FOUNDERS.map((f) => ({ who: f.id, name: f.name, channels: f.channels }))].flatMap((g) =>
g.channels.map((c) => {
const done = did(s.actions, 'follow', `${g.who}/${c.kind}`)
return (
<Action key={`${g.who}/${c.kind}`} render="button" type="button" fill={done} onClick={() => follow(g.who, c.kind, c.href)}>
{done ? <Check size={14} aria-hidden /> : null}
{g.who === 'hanzo' ? c.label : `${g.name} · ${c.label}`}
</Action>
)
}),
)}
</XStack>
</Card>
</YStack>
)
}