mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Calculator, captable, careers, cloud, compare, contact and contact-sales, customers, dashboards, dataroom, datastore, dev, download, enso, enterprise, extension, family, integrations, investors, kai, learn, legal and their components say their words through t() in client code, <T> in server components and phrase() where copy is held as data; one sentence is one key, figures and links its arguments. Rebased on main: in every conflict main's file stands (its copy, layout and styling) and only the wrapping is added back, including copy main added since. Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
231 lines
7.9 KiB
TypeScript
231 lines
7.9 KiB
TypeScript
import type { ComponentProps, ReactElement, ReactNode } from 'react'
|
||
import Link from 'next/link'
|
||
import { notFound } from 'next/navigation'
|
||
import { T } from '@hanzo/i18n/react'
|
||
import { featured } from '@/lib/data/customers'
|
||
import { testimonials } from '@/lib/data/testimonials'
|
||
import { pageMeta } from '@/lib/page-meta'
|
||
import { Box, Text, View, XStack } from '@/components/ui'
|
||
|
||
const told = featured.filter((s) => s.full)
|
||
|
||
export const dynamicParams = false
|
||
|
||
export function generateStaticParams() {
|
||
return told.map((s) => ({ id: s.id }))
|
||
}
|
||
|
||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
|
||
const { id } = await params
|
||
const s = told.find((x) => x.id === id)
|
||
if (!s?.full) return {}
|
||
return pageMeta({ title: `${s.company} · ${s.figures[0].value} ${s.figures[0].label}`, description: s.full.lede, path: `/customers/${s.id}` })
|
||
}
|
||
|
||
type TextProps = ComponentProps<typeof Text>
|
||
|
||
/** The page's measure and gutter. */
|
||
const WRAP = { width: '100%', maxW: 1080, mx: 'auto', px: 24 } as const
|
||
|
||
/** As many columns as fit at `min` wide, never wider than the page. */
|
||
const cells = (min: number) => `repeat(auto-fit, minmax(min(100%, ${min}px), 1fr))`
|
||
|
||
/** The mono small caps over every part: a date, a section, a chapter number. */
|
||
function Label({ children, ...p }: { children: ReactNode } & Omit<TextProps, 'children'>) {
|
||
return (
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontFamily="$mono"
|
||
fontSize="$1"
|
||
lineHeight="1.25rem"
|
||
letterSpacing={0.88}
|
||
textTransform="uppercase"
|
||
color="var(--muted-foreground)"
|
||
{...p}
|
||
>
|
||
{children}
|
||
</Text>
|
||
)
|
||
}
|
||
|
||
/** A paragraph of the story, at the relaxed leading. */
|
||
function Body({ children, ...p }: { children: ReactNode } & Omit<TextProps, 'children'>) {
|
||
return (
|
||
<Text render="p" display="block" fontSize="$3" lineHeight="1.4219rem" color="var(--muted-foreground)" {...p}>
|
||
{children}
|
||
</Text>
|
||
)
|
||
}
|
||
|
||
/** A picture with the page's hairline and a small corner. */
|
||
function Picture({ src, alt, lazy, ...p }: { src: string; alt: string; lazy?: boolean } & ComponentProps<typeof View>) {
|
||
return (
|
||
<View
|
||
render={<img src={src} alt={alt} loading={lazy ? 'lazy' : undefined} />}
|
||
display="block"
|
||
width="100%"
|
||
height="auto"
|
||
rounded={8}
|
||
borderWidth={1}
|
||
borderColor="var(--border)"
|
||
{...p}
|
||
/>
|
||
)
|
||
}
|
||
|
||
/** A hairline box on the page's first surface: a figure, a way out. */
|
||
const BOX = { borderWidth: 1, borderColor: 'var(--border)', rounded: 'var(--radius-lg, 12px)' } as const
|
||
const SURFACE = { backgroundColor: 'var(--surface-1, transparent)' } as const
|
||
|
||
/** A way out of the story, as a quiet bordered link. */
|
||
function Out({ to, children }: { to: ReactElement; children: ReactNode }) {
|
||
return (
|
||
<Text
|
||
render={to}
|
||
display="block"
|
||
{...BOX}
|
||
py={12}
|
||
px={18}
|
||
fontSize="$2"
|
||
lineHeight="1.25rem"
|
||
color="var(--foreground)"
|
||
textDecorationLine="none"
|
||
$platform-web={SURFACE}
|
||
>
|
||
{children}
|
||
</Text>
|
||
)
|
||
}
|
||
|
||
export default async function Story({ params }: { params: Promise<{ id: string }> }) {
|
||
const { id } = await params
|
||
const s = told.find((x) => x.id === id)
|
||
if (!s?.full) notFound()
|
||
const f = s.full
|
||
const t = testimonials.find((q) => s.company.startsWith(q.company))
|
||
return (
|
||
<Box render="main" pt={64} pb={96}>
|
||
<Box render="header" {...WRAP}>
|
||
<Label render={<Link href="/customers" />} display="inline-flex" textDecorationLine="none">
|
||
<T>{'← Customers'}</T>
|
||
</Label>
|
||
<Label mt={28}>
|
||
{s.year}
|
||
{s.year && s.kind ? ' · ' : null}
|
||
{s.kind ? <T>{s.kind}</T> : null}
|
||
</Label>
|
||
<Text
|
||
render="h1"
|
||
display="block"
|
||
mt={12}
|
||
mb={16}
|
||
fontWeight="600"
|
||
color="var(--foreground)"
|
||
$platform-web={{ fontSize: 'clamp(var(--text-4xl), 5vw, var(--text-7xl))', lineHeight: '1.05', letterSpacing: '-0.03em' }}
|
||
>
|
||
{s.company}
|
||
</Text>
|
||
<Body maxW={720} fontSize="$4" lineHeight="1.5234rem">
|
||
<T>{f.lede}</T>
|
||
</Body>
|
||
<Picture src={f.cover.src} alt={f.cover.alt} mt={32} aspectRatio={21 / 9} $platform-web={{ objectFit: 'cover' }} />
|
||
<View display="grid" gridTemplateColumns={cells(200)} gap={12} mt={12}>
|
||
{s.figures.map((x) => (
|
||
<Box key={x.label} {...BOX} p={18} $platform-web={SURFACE}>
|
||
<Text render="p" display="block" fontSize="$8" lineHeight="1.95rem" fontWeight="600" color="var(--foreground)" $platform-web={{ fontVariantNumeric: 'tabular-nums' }}>
|
||
{x.value}
|
||
</Text>
|
||
<Body mt={4} fontSize="$2" lineHeight="1.3203rem">
|
||
<T>{x.label}</T>
|
||
</Body>
|
||
</Box>
|
||
))}
|
||
</View>
|
||
</Box>
|
||
|
||
{f.report ? (
|
||
<View render="section" {...WRAP} mt={64} display="grid" gap={32} items="center" gridTemplateColumns={cells(360)}>
|
||
<Box>
|
||
<Label render="h2" fontWeight="700">
|
||
<T>{'The platform’s own report'}</T>
|
||
</Label>
|
||
<Body mt={12}>
|
||
<T>The campaign as Hanzo reported it after the first month: reach, spend, bikes presold and sold, and engagement by channel.</T>
|
||
</Body>
|
||
</Box>
|
||
<Picture src={f.report.src} alt={f.report.alt} />
|
||
</View>
|
||
) : null}
|
||
|
||
<Box render="section" {...WRAP} mt={64}>
|
||
<Label render="h2" fontWeight="700">
|
||
<T>How it went</T>
|
||
</Label>
|
||
<View render="ol" display="grid" gap={40} mt={20} mb={0} p={0} $platform-web={{ listStyle: 'none' }}>
|
||
{f.chapters.map((c, i) => (
|
||
<View key={c.title} render="li" display="grid" gap={24} items="center" gridTemplateColumns={cells(360)}>
|
||
<Box>
|
||
<Label>{String(i + 1).padStart(2, '0')}</Label>
|
||
<Text render="h3" display="block" mt={6} mb={10} fontSize="$7" lineHeight="1.575rem" fontWeight="500" color="var(--foreground)">
|
||
<T>{c.title}</T>
|
||
</Text>
|
||
<Body>
|
||
<T>{c.body}</T>
|
||
</Body>
|
||
</Box>
|
||
{c.picture ? <Picture src={c.picture.src} alt={c.picture.alt} lazy maxH={420} $platform-web={{ objectFit: 'cover' }} /> : null}
|
||
</View>
|
||
))}
|
||
</View>
|
||
</Box>
|
||
|
||
{f.gallery.length ? (
|
||
<Box render="section" {...WRAP} mt={64}>
|
||
<Label render="h2" fontWeight="700">
|
||
<T>From the campaign</T>
|
||
</Label>
|
||
<View display="grid" gridTemplateColumns={cells(220)} gap={12} mt={20} items="flex-start">
|
||
{f.gallery.map((g) => (
|
||
<Picture key={g.src} src={g.src} alt={g.alt} lazy />
|
||
))}
|
||
</View>
|
||
</Box>
|
||
) : null}
|
||
|
||
{t ? (
|
||
<Box render="section" {...WRAP} mt={64}>
|
||
<Box render="figure" m={0} borderLeftWidth={1} borderColor="var(--border)" pl="max(16px, min(3vw, 32px))">
|
||
<Text
|
||
render="blockquote"
|
||
display="block"
|
||
m={0}
|
||
fontSize="$7"
|
||
lineHeight="1.8375rem"
|
||
color="var(--foreground)"
|
||
$platform-web={{ fontFamily: 'var(--font-serif, Georgia, serif)' }}
|
||
>
|
||
“<T>{t.quote}</T>”
|
||
</Text>
|
||
<Box render="figcaption" mt={16}>
|
||
<Body color="var(--foreground)">{t.author}</Body>
|
||
<Label>
|
||
<T>{t.role}</T>
|
||
</Label>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
) : null}
|
||
|
||
<XStack render="section" {...WRAP} mt={64} flexWrap="wrap" gap={12}>
|
||
<Out to={<a href={f.pdf} />}>
|
||
<T>The original case study (PDF)</T>
|
||
</Out>
|
||
<Out to={<Link href="/customers" />}>
|
||
<T>Every launch →</T>
|
||
</Out>
|
||
</XStack>
|
||
</Box>
|
||
)
|
||
}
|