Files
zeekay 51797774c6 beta: two buttons, Continue with plan until subscribed and Review application; Share is an icon on a phone
The portal's header offered Continue with plan, View plans and Review answers
(and Open app to a member already in the app). It offers Continue with plan to
a member who has not subscribed and Review application to everyone. On a phone
the link's Share button is its icon, so the link itself keeps the room.
2026-10-26 16:25:32 -07:00

172 lines
6.8 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 ? null : (
<Action href={checkoutUrl} fill>
{t('Continue with plan')}
<ArrowRight size={15} aria-hidden data-i18n-mirror />
</Action>
)}
{onEditApplication ? (
<Action render="button" type="button" onClick={onEditApplication}>
{t('Review application')}
</Action>
) : 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>
)
}