Files
zeekay 70349b34c7 beta: a post about one topic, with that topic's page and card
The share card offers Hanzo, the products and the betas the member asked
for, the rest under More by group. A topic picks the posts Shuffle offers,
the link (/beta/for/<topic>, tagged utm_content) and the card it unfurls
to: that page carries the topic's title, line and card, a canonical naming
itself and the one noindex (lib/publish SHARES), and sends a person on to
/beta with the query kept, so ref and the campaign land as before.

lib/hanzo/topics is the list, its words the site's and BETAS's own.
generate-og draws a card per topic from it, palette-quantized with sharp:
49 cards, 2.9 MB. The cards gate reads every card's rendered words.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 12:03:39 -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} asked={s.asked} /> : 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>
)
}