Files
zeekay 4fd349aa06 beta: the portal says less, and opting in and sharing are one press
Prose that carried no decision or fact is gone: the hero, the questions, the
gate and every step are a heading and at most a line. What scores is said once
(the leaderboard step), and every claim the cloud does not keep is out.

Step 1 is one share card (the member's link, Copy, the device's share sheet on
phones, seven channels as icons, Customize behind a text button) and a quiet
invite-by-email card. Step 3 is one searched, grouped list with one control a
row: On when `opens` says so, Get access when it does not, Requested at once
after the press. Stars and follows moved under the leaderboard as Earn more.

The 1,800-line dashboard is a shell over share, rank, betas and alpha; what
they read is one hook (standing.ts); the questions are data (program.ts).
Dead access, card, hint and referral components are deleted. Every press is
44px, every input too, and a gate walks each screen at 390px.

scripts/brands and scripts/prebuild are TypeScript now; the three marks the
share row draws ship in a JSON only /beta loads. /beta's title and card say
what the page says.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 03:29:13 -07:00

180 lines
8.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 { useState, type ReactNode } from 'react'
import { Check, Copy, Mail, Send, Share2 } from 'lucide-react'
import { Facebook, Linkedin, Twitter } from '@/components/brand'
import { Input, Text, XStack, YStack, toast } from '@hanzo/ui'
import { Action, Title } from '@hanzo/ui/marketing'
import { phrase } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
import { track } from '@hanzo/app/lib/tags'
import { tagged } from '@/lib/hanzo/referrals'
import { Card, Quiet } from './card'
import { Bluesky, Reddit, Ycombinator } from './marks'
const TEXT = phrase('I just joined the @hanzoai beta.')
/** A channel's own page to share from: where it sends the member's link, tagged for it. */
const CHANNELS: { id: string; name: string; icon: ReactNode; to: (link: string, text: string) => string }[] = [
{ id: 'x', name: 'X', icon: <Twitter size={18} />, to: (u, t) => `https://x.com/intent/post?text=${encodeURIComponent(t)}&url=${encodeURIComponent(u)}` },
{ id: 'linkedin', name: 'LinkedIn', icon: <Linkedin size={18} />, to: (u) => `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(u)}` },
{ id: 'facebook', name: 'Facebook', icon: <Facebook size={18} />, to: (u) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(u)}` },
{ id: 'bluesky', name: 'Bluesky', icon: <Bluesky size={18} />, to: (u, t) => `https://bsky.app/intent/compose?text=${encodeURIComponent(`${t} ${u}`)}` },
{ id: 'reddit', name: 'Reddit', icon: <Reddit size={18} />, to: (u, t) => `https://www.reddit.com/r/HanzoAI/submit?url=${encodeURIComponent(u)}&title=${encodeURIComponent(t)}` },
{ id: 'hn', name: 'Hacker News', icon: <Ycombinator size={18} />, to: (u, t) => `https://news.ycombinator.com/submitlink?u=${encodeURIComponent(u)}&t=${encodeURIComponent(t)}` },
{ id: 'email', name: 'Email', icon: <Mail size={18} />, to: (u, t) => `mailto:?subject=${encodeURIComponent(t)}&body=${encodeURIComponent(`${t}\n\n${u}`)}` },
]
/**
* Step 1: the member's link, once, and the ways out with it. Nothing renders
* until the link exists: a press before it would share no link at all. Every way
* out goes through `tagged` (`ref` and `utm_source=<channel>`), and a press is
* one `waitlist_shared` event named for its channel.
*/
export function Share({ link, code, program, rename }: { link: string; code: string; program: string; rename: (code: string) => Promise<string> }) {
const { t } = useI18n()
const [copied, setCopied] = useState(false)
const [naming, setNaming] = useState(false)
const [draft, setDraft] = useState('')
const [saving, setSaving] = useState(false)
const [invites, setInvites] = useState('')
const native = typeof navigator !== 'undefined' && typeof navigator.share === 'function'
const via = (channel: string) => tagged(link, channel, 'beta_referral')
const heard = (method: string) => track('waitlist_shared', { method, program, from: 'beta' })
const copy = async () => {
heard('copy')
try {
await navigator.clipboard.writeText(via('copy'))
setCopied(true)
window.setTimeout(() => setCopied(false), 2400)
} catch {
toast.error(t('Copy failed. Select the link and copy it.'))
}
}
const share = async () => {
heard('native')
await navigator.share({ title: 'Hanzo', text: t(TEXT), url: via('native') }).catch(() => undefined)
}
const save = async (e: React.FormEvent) => {
e.preventDefault()
const clean = draft.trim().replace(/[^a-zA-Z0-9_-]/g, '')
if (clean.length < 2 || clean.length > 32) return toast.error(t('2 to 32 letters, numbers or hyphens.'))
setSaving(true)
try {
await rename(clean)
setNaming(false)
} catch (err) {
toast.error(err instanceof Error ? err.message : t('Could not save the link.'))
} finally {
setSaving(false)
}
}
const invite = (e: React.FormEvent) => {
e.preventDefault()
const to = invites.split(/[\s,;]+/).filter((x) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(x))
if (!to.length) return toast.error(t('Enter an email address.'))
track('beta_network_invited', { count: to.length })
const url = via('invite')
const body = `${t('Hey,')}\n\n${t('I’m using Hanzo to build with agents and frontier models. You can get early access to the beta with my link:')}\n\n${url}`
window.location.href = `mailto:${to.join(',')}?subject=${encodeURIComponent(t('Join me in the Hanzo beta'))}&body=${encodeURIComponent(body)}`
setInvites('')
}
return (
<YStack gap={16}>
<Card data-slot="beta-share">
<Title quiet>{t('Your link')}</Title>
{link ? (
<>
<XStack items="center" gap={12} flexWrap="wrap">
<Text flex={1} minW={0} flexBasis={240} fontFamily="$mono" fontSize="$3" color="var(--foreground)" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
{link}
</Text>
<XStack gap={8}>
<Action render="button" type="button" fill onClick={() => void copy()}>
{copied ? <Check size={15} aria-hidden /> : <Copy size={15} aria-hidden />}
{copied ? t('Copied') : t('Copy link')}
</Action>
{native ? (
<Action render="button" type="button" onClick={() => void share()}>
<Share2 size={15} aria-hidden />
{t('Share')}
</Action>
) : null}
</XStack>
</XStack>
<XStack gap={8} flexWrap="wrap" role="group" aria-label={t('Share on')}>
{CHANNELS.map((c) => (
<Action
key={c.id}
render={<a href={c.to(via(c.id), t(TEXT))} target="_blank" rel="noopener noreferrer" aria-label={t('Share on {name}', { name: c.name })} title={c.name} onClick={() => heard(c.id)} />}
width={44}
height={44}
p={0}
items="center"
justify="center"
>
{c.icon}
</Action>
))}
</XStack>
{code ? (
naming ? (
<form onSubmit={save}>
<XStack items="center" gap={8} flexWrap="wrap">
<Text fontFamily="$mono" fontSize="$2" color="var(--muted-foreground)">
hanzo.ai/beta?ref=
</Text>
<Input height={44} flex={1} minW={140} maxW={240} placeholder="yourname" value={draft} onChange={(e) => setDraft(e.target.value)} />
<Action render="button" type="submit" fill disabled={saving}>
{t('Save')}
</Action>
<Action render="button" type="button" onClick={() => setNaming(false)}>
{t('Cancel')}
</Action>
</XStack>
</form>
) : (
<XStack>
<Quiet
render="button"
type="button"
onClick={() => {
setDraft(code)
setNaming(true)
}}
>
{t('Customize link')}
</Quiet>
</XStack>
)
) : null}
</>
) : null}
</Card>
<Card bg="transparent" data-slot="beta-invite">
<Title quiet>{t('Invite by email')}</Title>
<form onSubmit={invite}>
<XStack items="center" gap={10} flexWrap="wrap">
<Input height={44} flex={1} minW={0} flexBasis={240} inputMode="email" placeholder={t('[email protected]')} aria-label={t('Email addresses')} value={invites} onChange={(e) => setInvites(e.target.value)} />
<Action render="button" type="submit" disabled={!link}>
<Send size={15} aria-hidden />
{t('Send')}
</Action>
</XStack>
</form>
</Card>
<XStack>
<Quiet href="/referral">{t('Referral dashboard')}</Quiet>
</XStack>
</YStack>
)
}