Files
hanzo.ai/app/(marketing)/customers/[id]/page.tsx

218 lines
7.6 KiB
TypeScript

import type { ComponentProps, ReactElement, ReactNode } from 'react'
import Link from 'next/link'
import { notFound } from 'next/navigation'
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">
← Customers
</Label>
<Label mt={28}>{[s.year, s.kind].filter(Boolean).join(' · ')}</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">
{f.lede}
</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">
{x.label}
</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">
The platform&rsquo;s own report
</Label>
<Body mt={12}>
The campaign as Hanzo reported it after the first month: reach, spend, bikes presold and sold, and engagement by channel.
</Body>
</Box>
<Picture src={f.report.src} alt={f.report.alt} />
</View>
) : null}
<Box render="section" {...WRAP} mt={64}>
<Label render="h2" fontWeight="700">
How it went
</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)">
{c.title}
</Text>
<Body>{c.body}</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">
From the campaign
</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)' }}
>
&ldquo;{t.quote}&rdquo;
</Text>
<Box render="figcaption" mt={16}>
<Body color="var(--foreground)">{t.author}</Body>
<Label>{t.role}</Label>
</Box>
</Box>
</Box>
) : null}
<XStack render="section" {...WRAP} mt={64} flexWrap="wrap" gap={12}>
<Out to={<a href={f.pdf} />}>The original case study (PDF)</Out>
<Out to={<Link href="/customers" />}>Every launch →</Out>
</XStack>
</Box>
)
}