Files
zeekay 3a8a35d13c beta: the portal is the app's Betas pane, after a plan step
/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
2026-10-25 18:38:26 -07:00

328 lines
9.7 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 React, { useState, useEffect, useRef } from 'react'
import {
Share2,
Copy,
Check,
ChevronDown,
Sparkles,
ExternalLink,
} from 'lucide-react'
import {
Box,
Button,
Text,
Textarea,
XStack,
YStack,
toast,
} from '@hanzo/ui'
import { track } from '@hanzo/app/lib/tags'
import { mine, tagged } from '@/lib/hanzo/referrals'
export interface ShareWidgetProps {
/** The title displayed in the card header. */
title?: string
/** Default text prefilled in the textarea. */
defaultText?: string
/** Base URL to share. Defaults to current window.location.href */
url?: string
/** Handle to mention on social platforms. Defaults to 'hanzoai'. */
handle?: string
/** Compact mode suitable for inline embedding or doc sidebars. */
compact?: boolean
}
interface SocialOption {
id: string
name: string
icon: string | React.ReactNode
getUrl: (url: string, text: string, handle: string) => string
}
const SOCIAL_OPTIONS: SocialOption[] = [
{
id: 'x',
name: '𝕏 @hanzoai',
icon: '𝕏',
getUrl: (url, text, handle) =>
`https://x.com/intent/post?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}&via=${encodeURIComponent(handle)}`,
},
{
id: 'facebook',
name: 'Facebook',
icon: 'f',
getUrl: (url) =>
`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
},
{
id: 'linkedin',
name: 'LinkedIn',
icon: 'in',
getUrl: (url) =>
`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
},
{
id: 'reddit',
name: 'Reddit',
icon: 'r/',
getUrl: (url, text) =>
`https://www.reddit.com/submit?url=${encodeURIComponent(url)}&title=${encodeURIComponent(text)}`,
},
{
id: 'threads',
name: 'Threads',
icon: '@',
getUrl: (url, text) =>
`https://www.threads.net/intent/post?text=${encodeURIComponent(`${text} ${url}`)}`,
},
{
id: 'bluesky',
name: 'Bluesky',
icon: '🦋',
getUrl: (url, text) =>
`https://bsky.app/intent/compose?text=${encodeURIComponent(`${text} ${url}`)}`,
},
{
id: 'mastodon',
name: 'Mastodon',
icon: '🐘',
getUrl: (url, text) =>
`https://mastodon.social/share?text=${encodeURIComponent(`${text} ${url}`)}`,
},
]
/**
* Universal Share Widget matching Hanzo's design system & sponsor share pattern.
*
* Automatically resolves the logged-in user's referral code via `mine()`
* and appends `?ref=<code>` to the target share URL for viral attribution.
*/
export function ShareWidget({
title = "Share Hanzo",
defaultText = "Exploring Hanzo AI frontier models and agentic platform — 1M context, vision, and native reasoning.",
url: propUrl,
handle = 'hanzoai',
compact = false,
}: ShareWidgetProps) {
const [text, setText] = useState(defaultText)
const [shareUrl, setShareUrl] = useState(propUrl || '')
const [refCode, setRefCode] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// 1. Resolve base URL
const rawUrl = propUrl || (typeof window !== 'undefined' ? window.location.href : 'https://hanzo.ai')
// 2. Fetch user's referral code to embed into share link
let active = true
mine()
.then((res) => {
if (!active) return
if (res.code) {
setRefCode(res.code)
try {
const u = new URL(rawUrl)
u.searchParams.set('ref', res.code)
setShareUrl(u.toString())
} catch {
setShareUrl(`${rawUrl}?ref=${res.code}`)
}
} else {
setShareUrl(rawUrl)
}
})
.catch(() => {
if (active) setShareUrl(rawUrl)
})
return () => {
active = false
}
}, [propUrl])
// Close dropdown on outside click
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setDropdownOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const handleCopy = async () => {
track('waitlist_shared', { method: 'copy', from: 'page', ref: refCode ?? '' })
try {
await navigator.clipboard.writeText(`${text}\n\n${tagged(shareUrl, 'copy', 'share')}`)
setCopied(true)
toast.success('Share link and text copied to clipboard!')
setTimeout(() => setCopied(false), 2500)
} catch {
toast.error('Unable to copy to clipboard')
}
}
const handleSocialClick = (opt: SocialOption) => {
track('waitlist_shared', { method: opt.id, from: 'page', ref: refCode ?? '' })
setDropdownOpen(false)
const targetUrl = opt.getUrl(tagged(shareUrl, opt.id, 'share'), text, handle)
window.open(targetUrl, '_blank', 'noopener,noreferrer,width=600,height=500')
}
return (
<YStack
bg="var(--panel, #0a0a0a)"
borderWidth={1}
borderColor="var(--border, #222)"
rounded={16}
p={compact ? 16 : 24}
gap={16}
maxW={520}
width="100%"
>
{/* Header */}
<XStack items="center" gap={12}>
<Box
width={36}
height={36}
rounded={10}
bg="var(--surface-2, rgba(255,255,255,0.06))"
borderWidth={1}
borderColor="var(--border, rgba(255,255,255,0.1))"
items="center"
justify="center"
>
<Sparkles size={18} color="var(--foreground)" />
</Box>
<YStack gap={2}>
<Text fontSize="$4" fontWeight="600" color="var(--foreground)">
{title}
</Text>
{refCode ? (
<Text fontSize="$1" color="var(--muted-foreground)">
Referral active: <Text color="var(--foreground)" fontWeight="500">?ref={refCode}</Text>
</Text>
) : (
<Text fontSize="$1" color="var(--muted-foreground)">
Share with your network on social media
</Text>
)}
</YStack>
</XStack>
{/* Editable Share Text */}
<Textarea
value={text}
onChange={(e: any) => setText(e.target.value)}
rows={compact ? 2 : 3}
bg="var(--surface-1, rgba(255,255,255,0.03))"
borderColor="var(--border, rgba(255,255,255,0.1))"
color="var(--foreground)"
fontSize="$3"
rounded={12}
p={12}
$platform-web={{ resize: 'vertical' }}
/>
{/* Action Bar */}
<XStack items="center" justify="space-between" gap={12}>
{/* Dropdown Menu Container */}
<Box position="relative" ref={dropdownRef}>
<Button
type="button"
gap={8}
height={38}
px={14}
rounded={10}
bg="var(--surface-2, rgba(255,255,255,0.08))"
hoverStyle={{ bg: 'var(--surface-3, rgba(255,255,255,0.14))' }}
borderColor="var(--border, rgba(255,255,255,0.15))"
borderWidth={1}
onClick={() => setDropdownOpen((v) => !v)}
>
<Share2 size={15} />
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
Share
</Text>
<ChevronDown size={14} />
</Button>
{dropdownOpen && (
<Box
position="absolute"
t="100%"
l={0}
mt={6}
bg="var(--background, #090909)"
borderWidth={1}
borderColor="var(--border, #262626)"
rounded={12}
py={6}
width={200}
style={{ zIndex: 100 }}
shadowColor="#000"
shadowOffset={{ width: 0, height: 8 }}
shadowOpacity={0.4}
shadowRadius={16}
>
{SOCIAL_OPTIONS.map((opt) => (
<XStack
key={opt.id}
render={<button type="button" onClick={() => handleSocialClick(opt)} />}
items="center"
justify="space-between"
width="100%"
px={14}
py={9}
gap={10}
bg="transparent"
borderWidth={0}
cursor="pointer"
hoverStyle={{ bg: 'var(--surface-2, rgba(255,255,255,0.06))' }}
$platform-web={{ textAlign: 'left' }}
>
<XStack items="center" gap={10}>
<Text fontSize="$2" fontFamily="$mono" fontWeight="700" width={18}>
{opt.icon}
</Text>
<Text fontSize="$2" color="var(--foreground)">
{opt.name}
</Text>
</XStack>
<ExternalLink size={12} color="var(--muted-foreground)" />
</XStack>
))}
</Box>
)}
</Box>
{/* Copy Link Button */}
<Button
type="button"
gap={8}
height={38}
px={14}
rounded={10}
bg="transparent"
borderColor="var(--border, rgba(255,255,255,0.12))"
borderWidth={1}
hoverStyle={{ borderColor: 'var(--foreground)' }}
onClick={handleCopy}
>
{copied ? <Check size={15} color="#22c55e" /> : <Copy size={15} />}
<Text fontSize="$2" fontWeight="500" color={copied ? '#22c55e' : 'var(--foreground)'}>
{copied ? 'Copied' : 'Copy link'}
</Text>
</Button>
</XStack>
</YStack>
)
}
export default ShareWidget