mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Share draws the link in a single field with Copy at its end, the channels in one row with Customize link at its other end. Rules is one component, drawn by the board and by /referral before it asks for an account, beside three steps that name what a link does and what scores. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
183 lines
7.8 KiB
TypeScript
183 lines
7.8 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
|
|
|
|
/** What scores, as five tiles and the one sentence about ties: the board's own, and the page of the same name's before it asks for an account. */
|
|
export function Rules() {
|
|
const { t } = useI18n()
|
|
return (
|
|
<YStack gap={12}>
|
|
<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>
|
|
)
|
|
}
|
|
|
|
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>
|
|
|
|
<Rules />
|
|
|
|
<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>
|
|
)
|
|
}
|