mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
132 lines
5.8 KiB
TypeScript
132 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { ArrowRight } from 'lucide-react'
|
|
import { Text, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Action, Chip, Title } from '@hanzo/ui/marketing'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
import { useIam } from '@hanzo/iam/react'
|
|
import { scope } from '@hanzo/app/lib/session'
|
|
import { useHydrated } from '@hanzo/app/lib/hydrated'
|
|
import { Box } from '@/components/ui'
|
|
import { Card, Quiet } from '@/components/beta/card'
|
|
import { Rank } from '@/components/beta/rank'
|
|
import { Rules, useCap } from '@/components/rewards/rules'
|
|
import { Posts } from '@/components/rewards/share'
|
|
import { useRules } from '@/components/rewards/use'
|
|
import { kind } from '@/lib/hanzo/rewards'
|
|
import { Share } from '@/components/beta/share'
|
|
import { useStanding } from '@/components/beta/standing'
|
|
import { BOOKING } from '@/components/home/nav-data'
|
|
|
|
|
|
/**
|
|
* /referral — the member's referral link and their place on the board, on a
|
|
* page of their own: the same link, points and board the member sees at /beta
|
|
* and /leaderboard, read from the same place (`useStanding`, the rewards
|
|
* ledger). Nothing is said here that the cloud does not keep.
|
|
*/
|
|
export default function Referral() {
|
|
const { t } = useI18n()
|
|
const hydrated = useHydrated()
|
|
const { user, isAuthenticated } = useIam()
|
|
const signed = hydrated && Boolean(isAuthenticated && scope().Authorization)
|
|
const s = useStanding(signed)
|
|
const rules = useRules()
|
|
const cap = useCap()
|
|
const email = String(user?.email || '')
|
|
const handle = s.code ? `@${s.code}` : email ? `@${email.split('@')[0]}` : t('you')
|
|
// What a referral earns, in the rules' numbers: a first task, then a return.
|
|
const active = kind(rules, 'active')?.points ?? 0
|
|
const retained = kind(rules, 'retained')?.points ?? 0
|
|
const limit = rules.caps.find((c) => c.source === 'referrals')
|
|
const climb = t('{active} points when a builder from your link runs a first AI task, and {retained} more when they are active again {days} days later.', { active, retained, days: rules.activity.retainDays })
|
|
const steps = [
|
|
{ head: t('Get your link'), body: t('One per account, made when you sign in. You can rename it.') },
|
|
{ head: t('Share it'), body: t('In a post, a message or an email. Every way out carries your link.') },
|
|
{ head: t('Climb the board'), body: limit ? `${climb} ${cap(rules, limit)}` : climb },
|
|
]
|
|
|
|
return (
|
|
<Box minH="100vh" pt={72} pb={96} px="var(--page-gutter)">
|
|
<YStack data-motion="rise" gap={16} width="100%" maxW={960} mx="auto" data-slot="referral">
|
|
<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">{t('Invite builders. Climb the board.')}</Title>
|
|
<Text fontSize="$3" color="var(--muted-foreground)">
|
|
{climb}
|
|
</Text>
|
|
</YStack>
|
|
{signed ? (
|
|
<XStack gap={8} flexWrap="wrap" maxW="100%">
|
|
<Chip px={14} py={8}>
|
|
{t('{n} pts', { n: s.total })}
|
|
</Chip>
|
|
<Chip px={14} py={8}>
|
|
{s.place > 0 ? `#${s.place}` : '—'}
|
|
{s.of > 0 ? ` / ${s.of.toLocaleString()}` : ''}
|
|
</Chip>
|
|
<Chip px={14} py={8}>
|
|
{t('{n} referred', { n: s.referred })}
|
|
</Chip>
|
|
{s.me && s.me.pending > 0 ? (
|
|
<Chip px={14} py={8} data-slot="referral-pending">
|
|
{t('+{n} pending', { n: s.me.pending })}
|
|
</Chip>
|
|
) : null}
|
|
</XStack>
|
|
) : null}
|
|
</XStack>
|
|
{hydrated && !signed ? (
|
|
<XStack gap={10} flexWrap="wrap">
|
|
<Action href={`/signup?next=${encodeURIComponent('/referral')}`} fill>
|
|
{t('Create account')}
|
|
<ArrowRight size={16} aria-hidden data-i18n-mirror />
|
|
</Action>
|
|
<Action href={`/login?next=${encodeURIComponent('/referral')}`}>{t('Sign in')}</Action>
|
|
</XStack>
|
|
) : null}
|
|
</Card>
|
|
|
|
{hydrated && !signed ? (
|
|
<>
|
|
<Card gap={16} data-slot="referral-how">
|
|
<Title quiet>{t('How it works')}</Title>
|
|
<View display="grid" gap={12} gridTemplateColumns="repeat(1, minmax(0, 1fr))" $md={{ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' }}>
|
|
{steps.map((x, i) => (
|
|
<YStack key={x.head} gap={4}>
|
|
<Text fontFamily="$mono" fontSize="$2" color="var(--muted-foreground)">
|
|
{`0${i + 1}`}
|
|
</Text>
|
|
<Text fontSize="$4" fontWeight="600" color="var(--foreground)">
|
|
{x.head}
|
|
</Text>
|
|
<Text fontSize="$3" color="var(--muted-foreground)" data-slot="referral-step">
|
|
{x.body}
|
|
</Text>
|
|
</YStack>
|
|
))}
|
|
</View>
|
|
</Card>
|
|
<Card gap={16} data-slot="referral-scores">
|
|
<Title quiet>{t('What scores')}</Title>
|
|
<Rules rules={rules} sources={['referrals', 'share']} />
|
|
</Card>
|
|
</>
|
|
) : null}
|
|
{signed && s.link ? <Share link={s.link} code={s.code} program="beta" rename={s.rename} asked={s.asked} onward={false} /> : null}
|
|
{signed ? <Posts rules={rules} signed={signed} next="/referral" /> : null}
|
|
{signed ? <Rank s={s} handle={handle} /> : null}
|
|
|
|
<XStack gap={20} flexWrap="wrap">
|
|
<Quiet href="/leaderboard">{t('The leaderboard')}</Quiet>
|
|
<Quiet href="/beta">{t('The beta')}</Quiet>
|
|
<Quiet href={BOOKING} target="_blank" rel="noopener noreferrer">
|
|
{t('Book a call')}
|
|
</Quiet>
|
|
</XStack>
|
|
</YStack>
|
|
</Box>
|
|
)
|
|
}
|