mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
/beta's portal is one component now. A stranger has it in the site's frame; a member has the app in its Betas mode (@hanzo/app 0.2.27: no sidebar there) with the same portal as its pane: the form if there is no application, a plan step once after filing (Pro with the beta code, or stay on Free), then the dashboard. Every share link, on the dashboard, the referral card, /referral and the page share widget, goes out through tagged(): the sharer's ref and the channel as utm_source, so a visit, a lead and a signup each say who shared and where. The page share widget's tracking was a no-op; it records waitlist_shared now. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
479 lines
17 KiB
TypeScript
479 lines
17 KiB
TypeScript
'use client'
|
||
|
||
import React, { useEffect, useState } from 'react'
|
||
import {
|
||
Check,
|
||
Copy,
|
||
Edit3,
|
||
Gift,
|
||
Mail,
|
||
MessageCircle,
|
||
Share2,
|
||
Video,
|
||
Zap,
|
||
} from 'lucide-react'
|
||
import {
|
||
Box,
|
||
Button,
|
||
Input,
|
||
Label,
|
||
Text,
|
||
View,
|
||
XStack,
|
||
YStack,
|
||
toast,
|
||
} from '@hanzo/ui'
|
||
import { useAnalytics } from '@hanzo/event/react'
|
||
import { EVENTS } from '@hanzo/event'
|
||
import { VisuallyHidden } from '@hanzo/gui'
|
||
import { Line } from '@/components/type'
|
||
import { PANEL } from '@/components/referrals/panel'
|
||
import { updateCode, tagged } from '@/lib/hanzo/referrals'
|
||
import { scope } from '@hanzo/app/lib/session'
|
||
|
||
interface ReferralLinkProps {
|
||
referralLink: string
|
||
referralCode: string
|
||
}
|
||
|
||
const ReferralLink = ({ referralLink: initialLink, referralCode: initialCode }: ReferralLinkProps) => {
|
||
const [activeCode, setActiveCode] = useState(initialCode)
|
||
const [activeLink, setActiveLink] = useState(initialLink)
|
||
const [isCustomizing, setIsCustomizing] = useState(false)
|
||
const [customSlugInput, setCustomSlugInput] = useState(initialCode)
|
||
const [savingSlug, setSavingSlug] = useState(false)
|
||
|
||
const [emailInput, setEmailInput] = useState('')
|
||
const [copied, setCopied] = useState(false)
|
||
const analytics = useAnalytics()
|
||
|
||
useEffect(() => {
|
||
setActiveCode(initialCode)
|
||
setActiveLink(initialLink)
|
||
setCustomSlugInput(initialCode)
|
||
}, [initialCode, initialLink])
|
||
|
||
if (!scope().Authorization || !activeCode || !activeLink) {
|
||
return null
|
||
}
|
||
|
||
// The link as shared on one channel (lib/hanzo/referrals `tagged`).
|
||
const via = (channel: string) => tagged(activeLink, channel)
|
||
|
||
const shareMessage =
|
||
'Building with frontier AI models on @hanzoai: autonomous Dev coding agent, multimodal Studio, and Zen frontier reasoning. Join the beta with my link:'
|
||
|
||
const handleShare = () => {
|
||
navigator.clipboard.writeText(via('copy'))
|
||
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'link', refCode: activeCode })
|
||
setCopied(true)
|
||
toast.success('Referral link copied to clipboard!')
|
||
window.setTimeout(() => setCopied(false), 2400)
|
||
}
|
||
|
||
const handleNativeShare = async () => {
|
||
if (typeof navigator.share === 'function') {
|
||
try {
|
||
await navigator.share({
|
||
title: 'Hanzo AI',
|
||
text: shareMessage,
|
||
url: via('native'),
|
||
})
|
||
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'native_share', refCode: activeCode })
|
||
} catch {
|
||
// cancelled
|
||
}
|
||
} else {
|
||
handleShare()
|
||
}
|
||
}
|
||
|
||
const handleSaveCustomSlug = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
const clean = customSlugInput.trim().replace(/[^a-zA-Z0-9_-]/g, '')
|
||
if (clean.length < 2 || clean.length > 32) {
|
||
toast.error('Custom link must be between 2 and 32 characters (letters, numbers, hyphens)')
|
||
return
|
||
}
|
||
|
||
setSavingSlug(true)
|
||
try {
|
||
const updated = await updateCode(clean)
|
||
const savedCode = updated.code || clean.toUpperCase()
|
||
const newUrl = updated.link || `https://hanzo.ai/?ref=${savedCode}`
|
||
setActiveCode(savedCode)
|
||
setActiveLink(newUrl)
|
||
setCustomSlugInput(savedCode)
|
||
setIsCustomizing(false)
|
||
toast.success(`Custom link active: hanzo.ai/?ref=${savedCode}`)
|
||
} catch (err) {
|
||
toast.error(err instanceof Error ? err.message : 'Could not save custom link. Please try again.')
|
||
} finally {
|
||
setSavingSlug(false)
|
||
}
|
||
}
|
||
|
||
const handleResetToDefault = () => {
|
||
setIsCustomizing(false)
|
||
setCustomSlugInput(activeCode)
|
||
}
|
||
|
||
const handleSendInvites = (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
const emails = emailInput
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
|
||
if (emails.length === 0) {
|
||
toast.error('Please enter at least one email address')
|
||
return
|
||
}
|
||
|
||
const subject = 'Join me on Hanzo'
|
||
const body = `Hey,\n\nI am using Hanzo AI for autonomous coding (Hanzo Dev), multimodal generation, and frontier reasoning models. Sign up with my referral link to get early access and free tier credits:\n\n${via('email')}\n\nCheers!`
|
||
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'email', count: emails.length, refCode: activeCode })
|
||
window.location.href = `mailto:${emails.map(encodeURIComponent).join(',')}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
|
||
}
|
||
|
||
const handleTikTokShare = async () => {
|
||
const tiktokSnippet = `Building on @hanzoinc: autonomous AI coding & frontier reasoning. Free access link in bio: ${via('tiktok')} #techtok #coding #ai #HanzoAI #developer`
|
||
try {
|
||
await navigator.clipboard.writeText(tiktokSnippet)
|
||
toast.success('Copied TikTok bio/caption snippet! Paste into your TikTok bio or video caption.')
|
||
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'tiktok', refCode: activeCode })
|
||
window.open('https://www.tiktok.com/@hanzoinc', '_blank')
|
||
} catch {
|
||
handleShare()
|
||
}
|
||
}
|
||
|
||
const handleYouTubeShare = async () => {
|
||
const ytSnippet = `Try Hanzo AI (@HanzoAI) for autonomous coding and frontier reasoning models: ${via('youtube')}\n\n#AI #Coding #SoftwareEngineering #Hanzo`
|
||
try {
|
||
await navigator.clipboard.writeText(ytSnippet)
|
||
toast.success('Copied YouTube description snippet with your link!')
|
||
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'youtube', refCode: activeCode })
|
||
window.open('https://youtube.com/@HanzoAI', '_blank')
|
||
} catch {
|
||
handleShare()
|
||
}
|
||
}
|
||
|
||
const socialChannels = [
|
||
{
|
||
name: '𝕏 @hanzoai',
|
||
channel: 'x',
|
||
icon: <Text fontWeight="700">𝕏</Text>,
|
||
href: `https://x.com/intent/post?text=${encodeURIComponent(shareMessage)}&url=${encodeURIComponent(via('x'))}`,
|
||
action: null,
|
||
},
|
||
{
|
||
name: 'LinkedIn',
|
||
channel: 'linkedin',
|
||
icon: <Text fontWeight="700">in</Text>,
|
||
href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(via('linkedin'))}`,
|
||
action: null,
|
||
},
|
||
{
|
||
name: 'Facebook',
|
||
channel: 'facebook',
|
||
icon: <Text fontWeight="700">f</Text>,
|
||
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(via('facebook'))}`,
|
||
action: null,
|
||
},
|
||
{
|
||
name: 'Bluesky',
|
||
channel: 'bluesky',
|
||
icon: <Text fontWeight="700">🦋</Text>,
|
||
href: `https://bsky.app/intent/compose?text=${encodeURIComponent(`${shareMessage} @hanzoai.bsky.social ${via('bluesky')}`)}`,
|
||
action: null,
|
||
},
|
||
{
|
||
name: 'Reddit r/HanzoAI',
|
||
channel: 'reddit',
|
||
icon: <Text fontWeight="700">r/</Text>,
|
||
href: `https://www.reddit.com/r/HanzoAI/submit?url=${encodeURIComponent(via('reddit'))}&title=${encodeURIComponent('Testing Hanzo AI frontier models and autonomous Dev coding agent')}`,
|
||
action: null,
|
||
},
|
||
{
|
||
name: 'TikTok @hanzoinc',
|
||
channel: 'tiktok',
|
||
icon: <Video size={16} aria-hidden />,
|
||
href: 'https://www.tiktok.com/@hanzoinc',
|
||
action: handleTikTokShare,
|
||
},
|
||
{
|
||
name: 'YouTube @HanzoAI',
|
||
channel: 'youtube',
|
||
icon: <Text fontWeight="700">▶</Text>,
|
||
href: 'https://youtube.com/@HanzoAI',
|
||
action: handleYouTubeShare,
|
||
},
|
||
{
|
||
name: 'Instagram @hanzoinc',
|
||
channel: 'instagram',
|
||
icon: <Text fontWeight="700">IG</Text>,
|
||
href: 'https://www.instagram.com/hanzoinc',
|
||
action: null,
|
||
},
|
||
{
|
||
name: 'Discord',
|
||
channel: 'discord',
|
||
icon: <MessageCircle size={15} aria-hidden />,
|
||
href: 'https://discord.gg/XthHQQj',
|
||
action: null,
|
||
},
|
||
{
|
||
name: 'Telegram',
|
||
channel: 'telegram',
|
||
icon: <MessageCircle size={15} aria-hidden />,
|
||
href: `https://t.me/share/url?url=${encodeURIComponent(via('telegram'))}&text=${encodeURIComponent(shareMessage)}`,
|
||
action: null,
|
||
},
|
||
{
|
||
name: 'WhatsApp',
|
||
channel: 'whatsapp',
|
||
icon: <Share2 size={15} aria-hidden />,
|
||
href: `https://api.whatsapp.com/send?text=${encodeURIComponent(`${shareMessage} ${via('whatsapp')}`)}`,
|
||
action: null,
|
||
},
|
||
]
|
||
|
||
return (
|
||
<YStack {...PANEL} gap={24}>
|
||
{/* Real-time Kickback Badges */}
|
||
<View
|
||
display="grid"
|
||
gap={12}
|
||
gridTemplateColumns="1fr"
|
||
$md={{ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' }}
|
||
>
|
||
<XStack items="center" gap={12} p={14} rounded={12} bg="var(--secondary)" borderWidth={1} borderColor="var(--border)">
|
||
<Zap size={20} color="#ffd700" style={{ flexShrink: 0 }} aria-hidden />
|
||
<YStack gap={2}>
|
||
<Text fontSize="$2" fontWeight="700" color="var(--foreground)">
|
||
10% Tokens Back (Lifetime Kickback)
|
||
</Text>
|
||
<Text fontSize="$1" color="var(--muted-foreground)">
|
||
Get 10% of all tokens consumed by people you refer credited back to your balance automatically as they build.
|
||
</Text>
|
||
</YStack>
|
||
</XStack>
|
||
|
||
<XStack items="center" gap={12} p={14} rounded={12} bg="var(--secondary)" borderWidth={1} borderColor="var(--border)">
|
||
<Gift size={20} color="var(--foreground)" style={{ flexShrink: 0 }} aria-hidden />
|
||
<YStack gap={2}>
|
||
<Text fontSize="$2" fontWeight="700" color="var(--foreground)">
|
||
1 Free Week Per Paid Plan
|
||
</Text>
|
||
<Text fontSize="$1" color="var(--muted-foreground)">
|
||
Every time someone you refer upgrades to a paid plan, unlock 1 full free week of Hanzo Pro or Enterprise.
|
||
</Text>
|
||
</YStack>
|
||
</XStack>
|
||
</View>
|
||
|
||
{/* 1. Primary Referral Link & Custom Slug Switcher */}
|
||
<YStack gap={16}>
|
||
<XStack items="center" justify="space-between" flexWrap="wrap" gap={8}>
|
||
<Line render="h2" size="xl" weight="500">
|
||
Your Personal Referral Link
|
||
</Line>
|
||
<XStack items="center" gap={8}>
|
||
<Text fontFamily="$mono" fontSize="$1" color="var(--muted-foreground)">
|
||
Code: {activeCode}
|
||
</Text>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => {
|
||
setCustomSlugInput(activeCode)
|
||
setIsCustomizing(!isCustomizing)
|
||
}}
|
||
>
|
||
<Edit3 size={13} style={{ marginRight: 4 }} aria-hidden />
|
||
{isCustomizing ? 'Close' : 'Customize Link'}
|
||
</Button>
|
||
</XStack>
|
||
</XStack>
|
||
|
||
{isCustomizing && (
|
||
<form onSubmit={handleSaveCustomSlug}>
|
||
<YStack gap={10} p={16} rounded={12} bg="var(--neutral-900)" borderWidth={1} borderColor="var(--neutral-700)">
|
||
<Text fontSize="$2" fontWeight="600" color="var(--foreground)">
|
||
Create Your Custom Vanity Referral Link
|
||
</Text>
|
||
<Text fontSize="$1" color="var(--muted-foreground)">
|
||
Set a memorable username, handle, or brand slug (e.g. yourname, techlead, devmemes).
|
||
</Text>
|
||
<XStack items="center" gap={8} flexWrap="wrap">
|
||
<Text fontFamily="$mono" fontSize="$2" color="var(--muted-foreground)">
|
||
https://hanzo.ai/?ref=
|
||
</Text>
|
||
<Box grow={1} minW={140} maxW={280}>
|
||
<Input
|
||
placeholder="custom-handle"
|
||
value={customSlugInput}
|
||
onChange={(e) => setCustomSlugInput(e.target.value)}
|
||
bg="var(--neutral-950)"
|
||
borderColor="var(--neutral-700)"
|
||
/>
|
||
</Box>
|
||
<Button type="submit" disabled={savingSlug}>
|
||
{savingSlug ? 'Saving…' : 'Save Custom Link'}
|
||
</Button>
|
||
{activeCode !== initialCode && (
|
||
<Button type="button" variant="outline" onClick={handleResetToDefault}>
|
||
Reset
|
||
</Button>
|
||
)}
|
||
</XStack>
|
||
</YStack>
|
||
</form>
|
||
)}
|
||
|
||
<YStack gap={12} $md={{ flexDirection: 'row' }}>
|
||
<Box position="relative" grow={1} shrink={1} flexBasis="0%">
|
||
<Input
|
||
value={activeLink}
|
||
readOnly
|
||
pr={48}
|
||
bg="var(--neutral-900)"
|
||
borderColor="var(--neutral-700)"
|
||
/>
|
||
</Box>
|
||
<Button gap={8} onClick={handleShare}>
|
||
{copied ? <Check width={16} height={16} /> : <Copy width={16} height={16} />}
|
||
{copied ? 'Copied Link!' : 'Copy Link'}
|
||
</Button>
|
||
<Button variant="outline" gap={8} onClick={handleNativeShare}>
|
||
<Share2 width={16} height={16} />
|
||
Share
|
||
</Button>
|
||
</YStack>
|
||
</YStack>
|
||
|
||
{/* 2. 1-Click Multi-Platform Social Sharing Buttons */}
|
||
<YStack gap={12} pt={20} borderTopWidth={1} borderColor="var(--neutral-800)">
|
||
<YStack gap={4}>
|
||
<Line render="h3" size="lg" weight="500">
|
||
Share Across All Platforms & Socials
|
||
</Line>
|
||
<Line size="sm" color="var(--muted-foreground)">
|
||
Post your custom link on TikTok, 𝕏, Reddit, YouTube, or LinkedIn to earn 10% token kickbacks and cash bounties.
|
||
</Line>
|
||
</YStack>
|
||
|
||
<View
|
||
display="grid"
|
||
gap={10}
|
||
gridTemplateColumns="repeat(2, minmax(0, 1fr))"
|
||
$sm={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
|
||
$md={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
|
||
>
|
||
{socialChannels.map((item) => {
|
||
if (item.action) {
|
||
return (
|
||
<XStack
|
||
key={item.channel}
|
||
render={<button type="button" onClick={item.action} />}
|
||
items="center"
|
||
justify="center"
|
||
gap={8}
|
||
height={44}
|
||
rounded={12}
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-700)"
|
||
bg="var(--neutral-900)"
|
||
cursor="pointer"
|
||
hoverStyle={{ borderColor: 'var(--foreground)', bg: 'var(--neutral-800)' }}
|
||
$platform-web={{ border: 'none', transition: 'all 140ms' }}
|
||
>
|
||
{item.icon}
|
||
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
|
||
{item.name}
|
||
</Text>
|
||
</XStack>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<XStack
|
||
key={item.channel}
|
||
render={
|
||
<a
|
||
href={item.href}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
onClick={() =>
|
||
analytics.capture(EVENTS.WAITLIST_SHARED, {
|
||
method: item.channel,
|
||
refCode: activeCode,
|
||
})
|
||
}
|
||
/>
|
||
}
|
||
items="center"
|
||
justify="center"
|
||
gap={8}
|
||
height={44}
|
||
rounded={12}
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-700)"
|
||
bg="var(--neutral-900)"
|
||
cursor="pointer"
|
||
hoverStyle={{ borderColor: 'var(--foreground)', bg: 'var(--neutral-800)' }}
|
||
$platform-web={{ textDecoration: 'none', color: 'var(--foreground)', transition: 'all 140ms' }}
|
||
>
|
||
{item.icon}
|
||
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
|
||
{item.name}
|
||
</Text>
|
||
</XStack>
|
||
)
|
||
})}
|
||
</View>
|
||
</YStack>
|
||
|
||
{/* 3. Direct Email Invite */}
|
||
<Box pt={20} borderTopWidth={1} borderColor="var(--neutral-800)">
|
||
<YStack gap={16}>
|
||
<YStack gap={4}>
|
||
<Line render="h3" size="lg" weight="500">
|
||
Invite by Email
|
||
</Line>
|
||
<Line size="sm" color="var(--muted-foreground)">
|
||
Opens your email client with your referral link pre-written.
|
||
</Line>
|
||
</YStack>
|
||
|
||
<form onSubmit={handleSendInvites}>
|
||
<YStack gap={16} $md={{ flexDirection: 'row' }}>
|
||
<Box grow={1} shrink={1} flexBasis="0%">
|
||
<VisuallyHidden>
|
||
<Label htmlFor="emails">Email Addresses</Label>
|
||
</VisuallyHidden>
|
||
<Input
|
||
id="emails"
|
||
placeholder="Enter email addresses, separated by commas"
|
||
value={emailInput}
|
||
onChange={(e) => setEmailInput(e.target.value)}
|
||
bg="var(--neutral-900)"
|
||
borderColor="var(--neutral-700)"
|
||
/>
|
||
</Box>
|
||
<Button type="submit" gap={8}>
|
||
<Mail width={16} height={16} />
|
||
Write Invite
|
||
</Button>
|
||
</YStack>
|
||
</form>
|
||
</YStack>
|
||
</Box>
|
||
</YStack>
|
||
)
|
||
}
|
||
|
||
export default ReferralLink
|