Files
zeekay 0cef879eca referral: the page is the beta's own link and board
/referral reads the same standing (useStanding) and draws the same Share and
Rank the signed-in /beta does: link, channels, invite by email, the five scoring
rules, the ranked waitlist, the people the link brought. Its header carries the
member's points, place and count. Share stops offering a way on to the page it
is on.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 09:57:01 -07:00

182 lines
8.1 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, onward = true }: { link: string; code: string; program: string; rename: (code: string) => Promise<string>; onward?: boolean }) {
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>
{onward ? (
<XStack>
<Quiet href="/referral">{t('Referral dashboard')}</Quiet>
</XStack>
) : null}
</YStack>
)
}