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