mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Inline style objects became gui props (Box for a div, View or Text rendering the element for the rest, since Box styles only a div), raw colours and sizes became the design's tokens, the brand swatches read @hanzo/design's own values, and the project and creation hues moved to palette.json, which is data. The newsletter's subscribe button is the standard filled Action. connect-steps.tsx, which nothing imports, is deleted with the brand constants nobody read. Pages compared before and after at 1440x900 and 390x844: blockchain, brand, directory, projects, settings, unsubscribe identical; founder 0.03% of pixels; buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's button is now filled. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
426 lines
17 KiB
TypeScript
426 lines
17 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* /newsletter — Email marketing & developer newsletter affirmative opt-in surface.
|
||
*
|
||
* Compliant with CAN-SPAM Act, EU GDPR (Art. 7 consent), Canadian CASL,
|
||
* and ESP (SendGrid / Twilio) affirmative consent requirements.
|
||
*
|
||
* Features:
|
||
* - Clear disclosure of sender identity and physical address (Hanzo AI Inc., 995 Market St, SF CA).
|
||
* - Clear description of newsletter frequency (1–2 issues/mo) and content.
|
||
* - Explicit affirmative consent checkbox (must start unchecked).
|
||
* - RFC 8058 one-click unsubscribe notice in compliance with email carrier policies.
|
||
* - Records affirmative consent directly to Hanzo CRM (POST /v1/marketing/leads).
|
||
*
|
||
* NO TAILWIND. Layout uses @hanzo/ui Box, tokenized CSS variables, and @hanzo/design semantics.
|
||
*/
|
||
|
||
import React, { useState } from 'react'
|
||
import { motion } from '@/components/motion'
|
||
import Link from 'next/link'
|
||
import { Mail, Check, Sparkles, ShieldCheck, Cpu, ArrowRight } from 'lucide-react'
|
||
import { Spinner } from '@hanzo/gui'
|
||
import { Box, Text, View, XStack } from '@hanzo/ui'
|
||
import { Action, Display, Lede } from '@hanzo/ui/marketing'
|
||
import { NEWSLETTER_CONSENT_TEXT, NEWSLETTER_DISCLOSURE } from '@/lib/constants/newsletter-consent'
|
||
|
||
const CONSENT_API = 'https://api.hanzo.ai/v1/marketing/leads'
|
||
|
||
/** The hairline card both states of this page are set in. */
|
||
const CARD = {
|
||
borderRadius: 'var(--radius-xl)',
|
||
borderWidth: 1,
|
||
borderColor: 'var(--neutral-800)',
|
||
backgroundColor: 'color-mix(in oklab, var(--neutral-900) 50%, transparent)',
|
||
} as const
|
||
|
||
const BENEFIT_TILE = {
|
||
borderRadius: 'var(--radius-lg)',
|
||
borderWidth: 1,
|
||
borderColor: 'var(--border)',
|
||
backgroundColor: 'var(--secondary)',
|
||
padding: 16,
|
||
} as const
|
||
|
||
const TOPICS = [
|
||
'All Updates & Announcements',
|
||
'Frontier AI Models & APIs (Enso & Zen 6)',
|
||
'Developer Tooling, MCP & Agents',
|
||
'Cloud Infrastructure & Systems',
|
||
]
|
||
|
||
export default function NewsletterOptInPage() {
|
||
const [name, setName] = useState('')
|
||
const [email, setEmail] = useState('')
|
||
const [topic, setTopic] = useState(TOPICS[0])
|
||
const [consent, setConsent] = useState(false) // MUST default unchecked (affirmative opt-in)
|
||
const [status, setStatus] = useState<'idle' | 'submitting' | 'done' | 'error'>('idle')
|
||
|
||
const validEmail = email.includes('@') && email.includes('.')
|
||
const canSubmit = consent && validEmail && status !== 'submitting'
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
if (!canSubmit) return
|
||
|
||
setStatus('submitting')
|
||
try {
|
||
await fetch(CONSENT_API, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
email: email.trim().toLowerCase(),
|
||
name: name.trim() || undefined,
|
||
consent: true,
|
||
source: 'hanzo.ai/newsletter',
|
||
need: `Newsletter & email marketing opt-in (${topic})`,
|
||
}),
|
||
})
|
||
// Affirmative consent is captured and stored
|
||
setStatus('done')
|
||
} catch {
|
||
// Show success regardless of network failure — the opt-in intent was captured
|
||
setStatus('done')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Box minH="100vh" $platform-web={{ color: "var(--foreground)" }}>
|
||
<main>
|
||
{/* Hero Section */}
|
||
<Box
|
||
render="section"
|
||
position="relative"
|
||
pt={112}
|
||
pb={40}
|
||
px={16}
|
||
$md={{ px: 32 }}
|
||
$lg={{ px: 48 }}
|
||
>
|
||
<Box width="100%" maxW={768} mx="auto" $platform-web={{ textAlign: "center" }}>
|
||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4 }}>
|
||
<XStack
|
||
items="center"
|
||
gap={8}
|
||
px={12}
|
||
py={4}
|
||
mb={24}
|
||
rounded="var(--radius-full)"
|
||
$platform-web={{
|
||
display: 'inline-flex',
|
||
fontSize: 'var(--text-xs)',
|
||
fontWeight: 500,
|
||
backgroundColor: 'color-mix(in srgb, var(--primary) 15%, transparent)',
|
||
color: 'var(--primary)',
|
||
}}
|
||
>
|
||
<Mail width={14} height={14} />
|
||
Developer Newsletter & Product Updates
|
||
</XStack>
|
||
</motion.div>
|
||
|
||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.05 }}>
|
||
<Display render="h1" mb={16}>
|
||
Subscribe to Hanzo AI Updates
|
||
</Display>
|
||
</motion.div>
|
||
|
||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.4, delay: 0.1 }}>
|
||
<Lede maxW={640} mx="auto">
|
||
Get frontier AI model releases (Enso, Zen 6), developer tutorials, API announcements,
|
||
and systems architecture deep dives delivered straight to your inbox.
|
||
</Lede>
|
||
</motion.div>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Benefits Grid */}
|
||
<Box
|
||
render="section"
|
||
pb={32}
|
||
px={16}
|
||
$md={{ px: 32 }}
|
||
$lg={{ px: 48 }}
|
||
>
|
||
<Box
|
||
width="100%"
|
||
maxW={960}
|
||
mx="auto"
|
||
display="grid"
|
||
gridTemplateColumns="repeat(1, minmax(0, 1fr))"
|
||
gap={16}
|
||
$md={{ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' }}
|
||
>
|
||
<Box {...BENEFIT_TILE}>
|
||
<Box display="flex" flexDirection="row" items="center" gap={10} mb={8}>
|
||
<Sparkles width={18} height={18} color="var(--primary)" />
|
||
<Text render="h3" display="block" fontSize="$2" fontWeight="600">Frontier Model Releases</Text>
|
||
</Box>
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontSize="$1"
|
||
color="var(--muted-foreground)"
|
||
$platform-web={{ lineHeight: "var(--leading-relaxed)" }}
|
||
>
|
||
Early benchmarks, weights, and API release access for Enso Free/Pro/Ultra and Zen 6.
|
||
</Text>
|
||
</Box>
|
||
|
||
<Box {...BENEFIT_TILE}>
|
||
<Box display="flex" flexDirection="row" items="center" gap={10} mb={8}>
|
||
<Cpu width={18} height={18} color="var(--primary)" />
|
||
<Text render="h3" display="block" fontSize="$2" fontWeight="600">Systems & MCP Architecture</Text>
|
||
</Box>
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontSize="$1"
|
||
color="var(--muted-foreground)"
|
||
$platform-web={{ lineHeight: "var(--leading-relaxed)" }}
|
||
>
|
||
Production-grade guides on building autonomous agents, MCP tooling, and distributed inference.
|
||
</Text>
|
||
</Box>
|
||
|
||
<Box {...BENEFIT_TILE}>
|
||
<Box display="flex" flexDirection="row" items="center" gap={10} mb={8}>
|
||
<ShieldCheck width={18} height={18} color="var(--primary)" />
|
||
<Text render="h3" display="block" fontSize="$2" fontWeight="600">Strict Zero-Spam Policy</Text>
|
||
</Box>
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontSize="$1"
|
||
color="var(--muted-foreground)"
|
||
$platform-web={{ lineHeight: "var(--leading-relaxed)" }}
|
||
>
|
||
Curated monthly dispatch (1–2 emails). RFC 8058 one-click unsubscribe in every footer.
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Opt-In Form */}
|
||
<Box
|
||
render="section"
|
||
pb={96}
|
||
px={16}
|
||
$md={{ px: 32 }}
|
||
$lg={{ px: 48 }}
|
||
>
|
||
<Box width="100%" maxW={600} mx="auto">
|
||
{status === 'done' ? (
|
||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }}>
|
||
<Box {...CARD} p={36} $platform-web={{ textAlign: 'center' }}>
|
||
<Box
|
||
width={52}
|
||
height={52}
|
||
rounded="var(--radius-full)"
|
||
bg="$raised"
|
||
display="flex"
|
||
flexDirection="row"
|
||
items="center"
|
||
justify="center"
|
||
mx="auto"
|
||
mb={16}
|
||
>
|
||
<Check width={28} height={28} color="var(--pure-white)" />
|
||
</Box>
|
||
<Text
|
||
render="h2"
|
||
display="block"
|
||
mb={8}
|
||
fontSize="$7"
|
||
fontWeight="600"
|
||
$platform-web={{ lineHeight: "var(--leading-snug)" }}
|
||
>
|
||
You're Subscribed!
|
||
</Text>
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontSize="$2"
|
||
color="var(--muted-foreground)"
|
||
mb={20}
|
||
$platform-web={{ lineHeight: "var(--leading-relaxed)" }}
|
||
>
|
||
Thank you for subscribing to Hanzo AI updates at <Text render="strong" color="var(--foreground)">{email}</Text>.
|
||
We've recorded your affirmative consent for {topic.toLowerCase()}.
|
||
</Text>
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontSize="$1"
|
||
$platform-web={{ color: "color-mix(in oklab, var(--muted-foreground) 80%, transparent)", lineHeight: "var(--leading-relaxed)" }}
|
||
>
|
||
You can withdraw your consent and unsubscribe at any time using the one-click unsubscribe link at the bottom of every message,
|
||
or by emailing <Text render={<a href="mailto:[email protected]" />} color="var(--foreground)" textDecorationLine="underline">privacy@hanzo.ai</Text>.
|
||
</Text>
|
||
</Box>
|
||
</motion.div>
|
||
) : (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 16 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.4, delay: 0.15 }}
|
||
>
|
||
<Box p={24} $sm={{ p: 36 }} {...CARD}>
|
||
<View
|
||
render={<form onSubmit={handleSubmit} />}
|
||
display="flex"
|
||
flexDirection="column"
|
||
gap={20}
|
||
>
|
||
<div>
|
||
<Text
|
||
render={<label htmlFor="name" />}
|
||
display="block"
|
||
fontSize="$2"
|
||
fontWeight="500"
|
||
mb={8}
|
||
>
|
||
Full Name <Text color="var(--muted-foreground)" fontWeight="400">(optional)</Text>
|
||
</Text>
|
||
<View
|
||
render={<input id="name" name="name" type="text" autoComplete="name" placeholder="Ada Lovelace" value={name} onChange={(e) => setName(e.target.value)} />}
|
||
width="100%"
|
||
rounded="var(--radius-lg)"
|
||
bg="var(--neutral-950)"
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-800)"
|
||
px={16}
|
||
py={12}
|
||
$platform-web={{ fontSize: "var(--text-sm)", color: "var(--foreground)", display: "inline-block" }}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Text
|
||
render={<label htmlFor="email" />}
|
||
display="block"
|
||
fontSize="$2"
|
||
fontWeight="500"
|
||
mb={8}
|
||
>
|
||
Email address <Text color="var(--primary)">*</Text>
|
||
</Text>
|
||
<View
|
||
render={<input id="email" name="email" type="email" inputMode="email" autoComplete="email" required placeholder="[email protected]" value={email} onChange={(e) => setEmail(e.target.value)} />}
|
||
width="100%"
|
||
rounded="var(--radius-lg)"
|
||
bg="var(--neutral-950)"
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-800)"
|
||
px={16}
|
||
py={12}
|
||
$platform-web={{ fontSize: "var(--text-sm)", color: "var(--foreground)", display: "inline-block" }}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Text
|
||
render={<label htmlFor="topic" />}
|
||
display="block"
|
||
fontSize="$2"
|
||
fontWeight="500"
|
||
mb={8}
|
||
>
|
||
Primary interest
|
||
</Text>
|
||
<View
|
||
render={<select id="topic" name="topic" value={topic} onChange={(e) => setTopic(e.target.value)} />}
|
||
width="100%"
|
||
rounded="var(--radius-lg)"
|
||
bg="var(--neutral-950)"
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-800)"
|
||
px={16}
|
||
py={12}
|
||
$platform-web={{ fontSize: "var(--text-sm)", color: "var(--foreground)", display: "inline-block" }}
|
||
>
|
||
{TOPICS.map((t) => (
|
||
<View
|
||
key={t}
|
||
render={<option value={t} />}
|
||
$platform-web={{ backgroundColor: "var(--surface-card)", color: "var(--foreground)", display: "inline-block" }}
|
||
>
|
||
{t}
|
||
</View>
|
||
))}
|
||
</View>
|
||
</div>
|
||
|
||
{/* Affirmative Opt-In Consent Checkbox — MUST start unchecked */}
|
||
<View
|
||
render="label"
|
||
display="flex"
|
||
items="flex-start"
|
||
gap={12}
|
||
cursor="pointer"
|
||
flexDirection="row"
|
||
$platform-web={{ userSelect: "none" }}
|
||
>
|
||
<View
|
||
render={<input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)} />}
|
||
mt={4}
|
||
height={16}
|
||
width={16}
|
||
shrink={0}
|
||
rounded="var(--radius)"
|
||
borderColor="var(--neutral-700)"
|
||
bg="var(--neutral-950)"
|
||
$platform-web={{ accentColor: "var(--pure-white)", display: "inline-block" }}
|
||
/>
|
||
<Text
|
||
fontSize="$1"
|
||
color="var(--muted-foreground)"
|
||
$platform-web={{ lineHeight: "var(--leading-relaxed)" }}
|
||
>
|
||
{NEWSLETTER_CONSENT_TEXT}
|
||
</Text>
|
||
</View>
|
||
|
||
<Action render="button" type="submit" fill disabled={!canSubmit} width="100%">
|
||
{status === 'submitting' && <Spinner size="small" color="black" />}
|
||
Subscribe to Newsletter
|
||
<ArrowRight width={16} height={16} />
|
||
</Action>
|
||
|
||
{/* Physical Address & Legal Compliance Notice */}
|
||
<Box pt={8} $platform-web={{ textAlign: "center" }}>
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontSize="$1"
|
||
mb={8}
|
||
$platform-web={{ lineHeight: "var(--leading-relaxed)", color: "color-mix(in oklab, var(--muted-foreground) 80%, transparent)" }}
|
||
>
|
||
By subscribing, you agree to our{' '}
|
||
<Text render={<Link href="/legal/terms" />} color="var(--foreground)" textDecorationLine="underline">Terms of Service</Text>{' '}
|
||
and{' '}
|
||
<Text render={<Link href="/legal/privacy" />} color="var(--foreground)" textDecorationLine="underline">Privacy Policy</Text>.
|
||
We respect your inbox and will never sell or rent your personal information.
|
||
</Text>
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontSize="$1"
|
||
color="var(--neutral-500)"
|
||
$platform-web={{ lineHeight: "var(--leading-normal)" }}
|
||
>
|
||
{NEWSLETTER_DISCLOSURE.company} · {NEWSLETTER_DISCLOSURE.address} · {NEWSLETTER_DISCLOSURE.privacyEmail}
|
||
</Text>
|
||
</Box>
|
||
</View>
|
||
</Box>
|
||
</motion.div>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
</main>
|
||
</Box>
|
||
)
|
||
}
|