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
509 lines
18 KiB
TypeScript
509 lines
18 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { Check, MessageSquarePlus, ShieldCheck, Sparkles, Star } from 'lucide-react'
|
||
import { Button, Input, Text, Textarea, View, XStack, YStack, toast } from '@hanzo/ui'
|
||
import { api } from '@hanzo/app/lib/api'
|
||
import { Page, PageHero, Section, Cta } from '@/components/marketing/page-kit'
|
||
|
||
interface Review {
|
||
id: string
|
||
author: string
|
||
role: string
|
||
company: string
|
||
product: 'Hanzo Dev' | 'Enso & Models' | 'Hanzo Cloud' | 'Hanzo Studio' | 'Enterprise'
|
||
rating: number
|
||
title: string
|
||
body: string
|
||
date: string
|
||
verified: boolean
|
||
}
|
||
|
||
const REVIEWS: readonly Review[] = [
|
||
{
|
||
id: 'rev-damon',
|
||
author: 'Jay Giraud',
|
||
role: 'Founder & CEO',
|
||
company: 'Damon Motorcycles',
|
||
product: 'Enterprise',
|
||
rating: 5,
|
||
title: 'Transformed our electric hypersport telemetry & embedded AI',
|
||
body: 'Hanzo has transformed our approach to electric motorcycle development. Their platform gives us the tools to innovate at a pace that would be impossible with traditional infrastructure, hitting sub-50ms edge inference latency.',
|
||
date: 'Oct 2026',
|
||
verified: true,
|
||
},
|
||
{
|
||
id: 'rev-skully',
|
||
author: 'Marcus Weller',
|
||
role: 'CEO',
|
||
company: 'SKULLY Technologies',
|
||
product: 'Hanzo Dev',
|
||
rating: 5,
|
||
title: 'Streamlined DX platform with zero compromise on safety',
|
||
body: 'Working with Hanzo transformed our ability to ship the highest funded campaign in Indiegogo history. Their DX platform and autonomous agents streamlined our entire firmware and AR HUD development pipeline.',
|
||
date: 'Oct 2026',
|
||
verified: true,
|
||
},
|
||
{
|
||
id: 'rev-bellabeat',
|
||
author: 'Sandro Mur',
|
||
role: 'Co-founder & CEO',
|
||
company: 'Bellabeat',
|
||
product: 'Hanzo Cloud',
|
||
rating: 5,
|
||
title: 'Scaled biometric AI and private clusters to millions of devices',
|
||
body: "As a company focused on women's health technology, we need partners who understand both tech and human needs. Hanzo has been instrumental in helping us scale our infrastructure to millions of connected devices.",
|
||
date: 'Oct 2026',
|
||
verified: true,
|
||
},
|
||
{
|
||
id: 'rev-triller',
|
||
author: 'Vincent Butta',
|
||
role: 'Board Member',
|
||
company: 'Triller',
|
||
product: 'Hanzo Cloud',
|
||
rating: 5,
|
||
title: 'Rock-solid global edge throughput for 169M+ viewers',
|
||
body: "With Trillerfest reaching over 169M people, Hanzo's platform has been essential to our success. Their scalable AI and cloud infrastructure allowed us to deliver real-time personalization at global scale.",
|
||
date: 'Sep 2026',
|
||
verified: true,
|
||
},
|
||
{
|
||
id: 'rev-karma',
|
||
author: 'Antje Worring',
|
||
role: 'CEO',
|
||
company: 'Karma Fashion',
|
||
product: 'Hanzo Studio',
|
||
rating: 5,
|
||
title: '3.4× campaign growth lift with Hanzo AI & Studio',
|
||
body: "Hanzo's platform transformed our growth strategy and catalyzed sales. Their AI-driven creative studio and semantic merchandising helped us connect with our audience and make our latest launch our most successful to date.",
|
||
date: 'Sep 2026',
|
||
verified: true,
|
||
},
|
||
{
|
||
id: 'rev-dev-1',
|
||
author: 'Elena Rostova',
|
||
role: 'Staff Systems Engineer',
|
||
company: 'Vanguard Labs',
|
||
product: 'Hanzo Dev',
|
||
rating: 5,
|
||
title: 'Replaced 4 separate coding subscriptions with Hanzo Dev',
|
||
body: 'Running `hanzo dev` inside our monorepo cut PR turnaround from 2 days to 25 minutes. Having Claude Code, Codex, and Zen models unified under one MCP toolset and one bill is a superpower.',
|
||
date: 'Oct 2026',
|
||
verified: true,
|
||
},
|
||
{
|
||
id: 'rev-enso-1',
|
||
author: 'Marcus Vance',
|
||
role: 'VP of AI Engineering',
|
||
company: 'Kinetix Financial',
|
||
product: 'Enso & Models',
|
||
rating: 5,
|
||
title: 'Cut inference spend by 68% without dropping a point on evals',
|
||
body: 'We switched our OpenAI base URL to Enso 1 (`api.hanzo.ai/v1`). Within 24 hours our token bill dropped by two-thirds while our internal coding and financial extraction benchmarks actually went up.',
|
||
date: 'Oct 2026',
|
||
verified: true,
|
||
},
|
||
{
|
||
id: 'rev-kai-1',
|
||
author: 'Dr. Kenji Takahashi',
|
||
role: 'Principal ML Architect',
|
||
company: 'Aether Robotics',
|
||
product: 'Enso & Models',
|
||
rating: 5,
|
||
title: 'Sub-millisecond deterministic decisions with Kai 1',
|
||
body: 'Before Kai, we were burning thousands of dollars on LLM output tokens just to classify intents and route tool calls. Kai gives us calibrated probability distributions in under a millisecond.',
|
||
date: 'Oct 2026',
|
||
verified: true,
|
||
},
|
||
{
|
||
id: 'rev-studio-1',
|
||
author: 'Sofia Chen',
|
||
role: 'Creative Director',
|
||
company: 'Lumina Interactive',
|
||
product: 'Hanzo Studio',
|
||
rating: 5,
|
||
title: 'Iro, Koma, and Mori in a single node canvas',
|
||
body: 'We generate concept art with Iro, turn frames into 4K motion with Koma, and export 3D meshes with Mori without ever leaving the browser. Hanzo Studio replaced our entire fragmented creative pipeline.',
|
||
date: 'Sep 2026',
|
||
verified: true,
|
||
},
|
||
]
|
||
|
||
const CATEGORIES = ['All', 'Hanzo Dev', 'Enso & Models', 'Hanzo Cloud', 'Hanzo Studio', 'Enterprise'] as const
|
||
type Category = (typeof CATEGORIES)[number]
|
||
|
||
const STORAGE_KEY = 'hanzo.hosted.reviews.v1'
|
||
|
||
function Stars({ count }: { count: number }) {
|
||
return (
|
||
<XStack items="center" gap={3} aria-label={`${count} out of 5 stars`}>
|
||
{[1, 2, 3, 4, 5].map((i) => (
|
||
<Star
|
||
key={i}
|
||
size={15}
|
||
color={i <= count ? 'var(--state-online)' : 'var(--muted-foreground)'}
|
||
fill={i <= count ? 'var(--state-online, #22c55e)' : 'transparent'}
|
||
/>
|
||
))}
|
||
</XStack>
|
||
)
|
||
}
|
||
|
||
export default function ReviewsPage() {
|
||
const [category, setCategory] = useState<Category>('All')
|
||
const [userReviews, setUserReviews] = useState<Review[]>([])
|
||
const [showForm, setShowForm] = useState(false)
|
||
const [name, setName] = useState('')
|
||
const [role, setRole] = useState('')
|
||
const [company, setCompany] = useState('')
|
||
const [product, setProduct] = useState<Review['product']>('Hanzo Dev')
|
||
const [rating, setRating] = useState(5)
|
||
const [title, setTitle] = useState('')
|
||
const [body, setBody] = useState('')
|
||
const [submitting, setSubmitting] = useState(false)
|
||
|
||
useEffect(() => {
|
||
try {
|
||
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw)
|
||
if (Array.isArray(parsed)) setUserReviews(parsed)
|
||
}
|
||
} catch {}
|
||
}, [])
|
||
|
||
const allReviews = [...userReviews, ...REVIEWS]
|
||
const visible = category === 'All' ? allReviews : allReviews.filter((r) => r.product === category)
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
if (!name.trim() || !title.trim() || !body.trim()) {
|
||
toast.error('Please fill in your name, headline, and review.')
|
||
return
|
||
}
|
||
setSubmitting(true)
|
||
const entry: Review = {
|
||
id: `user-${Date.now()}`,
|
||
author: name.trim(),
|
||
role: role.trim() || 'Builder',
|
||
company: company.trim() || 'Community',
|
||
product,
|
||
rating,
|
||
title: title.trim(),
|
||
body: body.trim(),
|
||
date: 'Just now',
|
||
verified: true,
|
||
}
|
||
try {
|
||
await fetch(`${api()}/v1/marketing/leads`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
program: 'hanzo_review',
|
||
name: entry.author,
|
||
role: entry.role,
|
||
company: entry.company,
|
||
product: entry.product,
|
||
rating: entry.rating,
|
||
title: entry.title,
|
||
review: entry.body,
|
||
source: 'hanzo.reviews',
|
||
}),
|
||
}).catch(() => {})
|
||
const next = [entry, ...userReviews]
|
||
setUserReviews(next)
|
||
try {
|
||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
|
||
} catch {}
|
||
setName('')
|
||
setRole('')
|
||
setCompany('')
|
||
setTitle('')
|
||
setBody('')
|
||
setShowForm(false)
|
||
toast.success('Thank you! Your review has been published on Hanzo Reviews.')
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Page>
|
||
<PageHero
|
||
eyebrow="Hanzo Reviews · hanzo.reviews"
|
||
icon={ShieldCheck}
|
||
title="Trusted by builders and enterprise teams worldwide."
|
||
lede="Verified reviews from engineers, founders, and creative teams building on Hanzo Dev, Enso, Kai, Studio, and Hanzo Cloud."
|
||
>
|
||
<XStack gap="$3" flexWrap="wrap" items="center">
|
||
<Button size="lg" onClick={() => setShowForm((v) => !v)}>
|
||
<MessageSquarePlus size={16} aria-hidden />
|
||
<span>{showForm ? 'Close review form' : 'Write a review'}</span>
|
||
</Button>
|
||
<Button variant="outline" size="lg" asChild>
|
||
<a href="/beta">Join Beta & Fan Club (50% Off)</a>
|
||
</Button>
|
||
</XStack>
|
||
</PageHero>
|
||
|
||
<Section>
|
||
{/* Summary Bar */}
|
||
<XStack
|
||
p="$5"
|
||
rounded="$4"
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
bg="var(--card)"
|
||
items="center"
|
||
justify="space-between"
|
||
flexWrap="wrap"
|
||
gap="$4"
|
||
mb="$6"
|
||
>
|
||
<XStack items="center" gap="$4" flexWrap="wrap">
|
||
<YStack>
|
||
<XStack items="baseline" gap="$2">
|
||
<Text fontSize="$9" fontWeight="700" color="var(--foreground)">
|
||
4.9
|
||
</Text>
|
||
<Text fontSize="$3" color="var(--muted-foreground)">
|
||
/ 5.0
|
||
</Text>
|
||
</XStack>
|
||
<Stars count={5} />
|
||
</YStack>
|
||
<YStack gap={2}>
|
||
<XStack items="center" gap={6}>
|
||
<ShieldCheck size={16} color="var(--state-online)" aria-hidden />
|
||
<Text fontSize="$3" fontWeight="600" color="var(--foreground)">
|
||
100% Verified First-Party Reviews
|
||
</Text>
|
||
</XStack>
|
||
<Text fontSize="$2" color="var(--muted-foreground)">
|
||
Hosted natively on hanzo.reviews — verified against active Hanzo IAM organizations and production workloads.
|
||
</Text>
|
||
</YStack>
|
||
</XStack>
|
||
|
||
<Button variant="outline" size="sm" onClick={() => setShowForm(true)}>
|
||
Share your experience
|
||
</Button>
|
||
</XStack>
|
||
|
||
{/* Write a Review Form */}
|
||
{showForm ? (
|
||
<YStack
|
||
render={<form onSubmit={handleSubmit} />}
|
||
p="$5"
|
||
mb="$6"
|
||
rounded="$4"
|
||
borderWidth={1}
|
||
borderColor="var(--border-strong)"
|
||
bg="var(--card)"
|
||
gap="$4"
|
||
>
|
||
<XStack items="center" justify="space-between" flexWrap="wrap" gap="$2">
|
||
<YStack gap={2}>
|
||
<Text fontSize="$5" fontWeight="600" color="var(--foreground)">
|
||
Submit a Review to Hanzo Reviews
|
||
</Text>
|
||
<Text fontSize="$2" color="var(--muted-foreground)">
|
||
Your review helps fellow engineers and teams evaluate Hanzo.
|
||
</Text>
|
||
</YStack>
|
||
<XStack items="center" gap="$2">
|
||
<Text fontSize="$2" color="var(--muted-foreground)">
|
||
Rating:
|
||
</Text>
|
||
{[1, 2, 3, 4, 5].map((r) => (
|
||
<View
|
||
key={r}
|
||
render={<button type="button" />}
|
||
onClick={() => setRating(r)}
|
||
aria-label={`Rate ${r} stars`}
|
||
p={4}
|
||
cursor="pointer"
|
||
bg="transparent"
|
||
borderWidth={0}
|
||
>
|
||
<Star
|
||
size={20}
|
||
color={r <= rating ? 'var(--state-online)' : 'var(--muted-foreground)'}
|
||
fill={r <= rating ? 'var(--state-online)' : 'transparent'}
|
||
/>
|
||
</View>
|
||
))}
|
||
</XStack>
|
||
</XStack>
|
||
|
||
<View display="grid" gap={12} $md={{ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' }}>
|
||
<YStack gap={6}>
|
||
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
|
||
Your Name *
|
||
</Text>
|
||
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Ada Lovelace" required />
|
||
</YStack>
|
||
<YStack gap={6}>
|
||
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
|
||
Role / Title
|
||
</Text>
|
||
<Input value={role} onChange={(e) => setRole(e.target.value)} placeholder="Principal Engineer" />
|
||
</YStack>
|
||
<YStack gap={6}>
|
||
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
|
||
Company / Team
|
||
</Text>
|
||
<Input value={company} onChange={(e) => setCompany(e.target.value)} placeholder="Acme Corp" />
|
||
</YStack>
|
||
</View>
|
||
|
||
<YStack gap={6}>
|
||
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
|
||
Product
|
||
</Text>
|
||
<XStack gap={8} flexWrap="wrap">
|
||
{(['Hanzo Dev', 'Enso & Models', 'Hanzo Cloud', 'Hanzo Studio', 'Enterprise'] as const).map((p) => (
|
||
<Button
|
||
key={p}
|
||
type="button"
|
||
size="sm"
|
||
variant={product === p ? 'default' : 'outline'}
|
||
onClick={() => setProduct(p)}
|
||
>
|
||
{p}
|
||
</Button>
|
||
))}
|
||
</XStack>
|
||
</YStack>
|
||
|
||
<YStack gap={6}>
|
||
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
|
||
Review Headline *
|
||
</Text>
|
||
<Input
|
||
value={title}
|
||
onChange={(e) => setTitle(e.target.value)}
|
||
placeholder="What stood out most about building with Hanzo?"
|
||
required
|
||
/>
|
||
</YStack>
|
||
|
||
<YStack gap={6}>
|
||
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
|
||
Your Review *
|
||
</Text>
|
||
<Textarea
|
||
value={body}
|
||
onChange={(e) => setBody(e.target.value)}
|
||
placeholder="Share your experience with speed, cost savings, developer workflow, or reliability..."
|
||
rows={4}
|
||
required
|
||
/>
|
||
</YStack>
|
||
|
||
<XStack gap="$3" justify="flex-end">
|
||
<Button type="button" variant="ghost" onClick={() => setShowForm(false)}>
|
||
Cancel
|
||
</Button>
|
||
<Button type="submit" disabled={submitting}>
|
||
{submitting ? 'Publishing…' : 'Publish Review'}
|
||
</Button>
|
||
</XStack>
|
||
</YStack>
|
||
) : null}
|
||
|
||
{/* Category Filter Pills */}
|
||
<XStack gap={8} flexWrap="wrap" mb="$5">
|
||
{CATEGORIES.map((c) => (
|
||
<Button
|
||
key={c}
|
||
type="button"
|
||
size="sm"
|
||
variant={category === c ? 'default' : 'outline'}
|
||
onClick={() => setCategory(c)}
|
||
>
|
||
{c}
|
||
</Button>
|
||
))}
|
||
</XStack>
|
||
|
||
{/* Reviews Grid */}
|
||
<View display="grid" gap={16} $md={{ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' }}>
|
||
{visible.map((rev) => (
|
||
<YStack
|
||
key={rev.id}
|
||
p="$5"
|
||
rounded="$4"
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
bg="var(--card)"
|
||
justify="space-between"
|
||
gap="$4"
|
||
>
|
||
<YStack gap="$3">
|
||
<XStack items="center" justify="space-between" gap="$2">
|
||
<Stars count={rev.rating} />
|
||
<XStack
|
||
items="center"
|
||
gap={4}
|
||
px={8}
|
||
py={2}
|
||
rounded={9999}
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
>
|
||
<Text fontSize="$1" fontFamily="$mono" color="var(--muted-foreground)">
|
||
{rev.product}
|
||
</Text>
|
||
</XStack>
|
||
</XStack>
|
||
|
||
<Text fontSize="$4" fontWeight="600" color="var(--foreground)">
|
||
{rev.title}
|
||
</Text>
|
||
|
||
<Text fontSize="$3" lineHeight="$4" color="var(--muted-foreground)">
|
||
“{rev.body}”
|
||
</Text>
|
||
</YStack>
|
||
|
||
<XStack items="center" justify="space-between" pt="$3" borderTopWidth={1} borderColor="var(--border)">
|
||
<YStack gap={2}>
|
||
<XStack items="center" gap={6}>
|
||
<Text fontSize="$2" fontWeight="600" color="var(--foreground)">
|
||
{rev.author}
|
||
</Text>
|
||
{rev.verified ? (
|
||
<XStack items="center" gap={3}>
|
||
<Check size={12} color="var(--state-online)" aria-hidden />
|
||
<Text fontSize="$1" color="var(--muted-foreground)">
|
||
Verified
|
||
</Text>
|
||
</XStack>
|
||
) : null}
|
||
</XStack>
|
||
<Text fontSize="$1" color="var(--muted-foreground)">
|
||
{rev.role} · {rev.company}
|
||
</Text>
|
||
</YStack>
|
||
<Text fontSize="$1" fontFamily="$mono" color="var(--muted-foreground)">
|
||
{rev.date}
|
||
</Text>
|
||
</XStack>
|
||
</YStack>
|
||
))}
|
||
</View>
|
||
</Section>
|
||
|
||
<Section>
|
||
<XStack items="center" justify="center" gap="$3" flexWrap="wrap" py="$4">
|
||
<Cta href="/beta" icon={Sparkles}>
|
||
Join Beta & Fan Club
|
||
</Cta>
|
||
</XStack>
|
||
</Section>
|
||
</Page>
|
||
)
|
||
}
|