mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.
172 lines
6.8 KiB
TypeScript
172 lines
6.8 KiB
TypeScript
'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>
|
||
)
|
||
}
|