Add theme toggle, hero snippet, and reorganize homepage components

- Set up next-themes with a light/dark toggle in the header and move all
  sections onto theme tokens; dark mode stays pure black
- Replace the hero terminal line with the AI Elements Snippet component
- Restructure components into layout/, sections/, and shared/ with
  site-wide config in lib/site.ts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rami Bitar
2026-09-18 09:29:12 -04:00
co-authored by Claude Fable 5.1
parent a40edbffa9
commit 07a99b441c
28 changed files with 812 additions and 437 deletions
+25
View File
@@ -0,0 +1,25 @@
import { ActionButtons } from '@/components/shared/action-buttons';
import { Container } from '@/components/shared/container';
import { GradientGlow } from '@/components/shared/gradient-glow';
import { SectionHeading } from '@/components/shared/section-heading';
export function CtaSection() {
return (
<section className="relative overflow-hidden">
<GradientGlow className="left-1/2 top-1/2 h-[300px] w-[600px] -translate-x-1/2 -translate-y-1/2 opacity-25" />
<Container className="relative py-28">
<SectionHeading
align="center"
title="Ship your first project in minutes."
description="Import a repository or start from a template. No credit card required to deploy your first preview."
className="[&>h2]:text-4xl sm:[&>h2]:text-5xl"
/>
<ActionButtons
className="mt-9"
primary={{ label: 'Start Deploying', href: '#' }}
secondary={{ label: 'Talk to Sales', href: '#' }}
/>
</Container>
</section>
);
}
+87
View File
@@ -0,0 +1,87 @@
import { Container } from '@/components/shared/container';
import { SectionHeading } from '@/components/shared/section-heading';
const features = [
{
step: '01',
title: 'Build',
description:
'Native support for 35+ frameworks. Push your code and Vertex handles the build pipeline, caching, and dependencies automatically.',
icon: <path d="M8 6L2 12l6 6M16 6l6 6-6 6" strokeWidth="1.5" fill="none" />,
},
{
step: '02',
title: 'Preview',
description:
'Every push gets a live preview URL. Share it, comment on it, and ship with confidence — no environment drift, no surprises.',
icon: (
<>
<rect
x="3"
y="4"
width="18"
height="14"
rx="2"
strokeWidth="1.5"
fill="none"
/>
<path d="M9 21h6M12 18v3" strokeWidth="1.5" fill="none" />
</>
),
},
{
step: '03',
title: 'Ship',
description:
'Deploy to a global edge network in seconds. Instant rollbacks, zero-downtime releases, and traffic that lands close to every user.',
icon: (
<path
d="M4 13l1.5-5.5A2 2 0 0 1 7.4 6h9.2a2 2 0 0 1 1.9 1.5L20 13M4 13v5h16v-5M4 13h16M8 17h.01M16 17h.01"
strokeWidth="1.5"
fill="none"
/>
),
},
];
export function FeaturesSection() {
return (
<section>
<Container className="py-24">
<SectionHeading
eyebrow="The workflow"
title="From commit to production, without the ceremony."
/>
<div className="mt-14 grid gap-px overflow-hidden rounded-2xl border border-border bg-border md:grid-cols-3">
{features.map((feature) => (
<div
key={feature.step}
className="group flex flex-col bg-background p-8 transition-colors hover:bg-foreground/[0.03]"
>
<div className="flex items-center justify-between">
<svg
width="28"
height="28"
viewBox="0 0 24 24"
stroke="currentColor"
className="text-foreground/80"
aria-hidden="true"
>
{feature.icon}
</svg>
<span className="font-mono text-xs text-foreground/30">
{feature.step}
</span>
</div>
<h3 className="mt-6 text-xl font-medium">{feature.title}</h3>
<p className="mt-3 text-sm leading-6 text-foreground/50">
{feature.description}
</p>
</div>
))}
</div>
</Container>
</section>
);
}
+82
View File
@@ -0,0 +1,82 @@
import {
Snippet,
SnippetAddon,
SnippetCopyButton,
SnippetInput,
SnippetText,
} from '@/components/ai-elements/snippet';
import { ActionButtons } from '@/components/shared/action-buttons';
import { Container } from '@/components/shared/container';
import { GradientGlow } from '@/components/shared/gradient-glow';
const hero = {
announcement: { label: 'Vertex Edge is now generally available', href: '#' },
title: 'Build. Preview. Ship.',
description:
'Vertex is the frontend cloud. Give your team the tooling and global infrastructure to build, preview, and ship a faster web — from your first commit to production traffic.',
primary: { label: 'Start Deploying', href: '#' },
secondary: { label: 'Get a Demo', href: '#' },
command: 'vertex deploy --prod',
};
function GridBackdrop() {
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,black,transparent)]"
style={{
backgroundImage:
'linear-gradient(to right, color-mix(in oklab, var(--foreground) 6%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in oklab, var(--foreground) 6%, transparent) 1px, transparent 1px)',
backgroundSize: '56px 56px',
}}
/>
);
}
export function HeroSection() {
return (
<section className="relative overflow-hidden">
<GridBackdrop />
<GradientGlow className="left-1/2 top-[-120px] h-[420px] w-[820px] -translate-x-1/2 opacity-40" />
<Container className="relative pt-24 pb-20 text-center sm:pt-32">
<a
href={hero.announcement.href}
className="group inline-flex items-center gap-2 rounded-full border border-foreground/15 bg-foreground/5 px-3.5 py-1.5 text-xs text-foreground/70 transition-colors hover:border-foreground/25 hover:text-foreground"
>
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-400" />
{hero.announcement.label}
<span className="transition-transform group-hover:translate-x-0.5">
&rarr;
</span>
</a>
<h1 className="mx-auto mt-8 max-w-4xl bg-gradient-to-b from-foreground to-foreground/60 bg-clip-text text-5xl font-semibold leading-[1.05] tracking-tight text-transparent sm:text-7xl">
{hero.title}
</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-foreground/55">
{hero.description}
</p>
<ActionButtons
className="mt-10"
primary={hero.primary}
secondary={hero.secondary}
/>
<Snippet
className="mx-auto mt-14 h-12 max-w-md rounded-xl border-border bg-foreground/[0.03] text-sm"
code={hero.command}
>
<SnippetAddon className="pl-1">
<SnippetText>$</SnippetText>
</SnippetAddon>
<SnippetInput />
<SnippetAddon align="inline-end" className="pr-2">
<SnippetCopyButton />
</SnippetAddon>
</Snippet>
</Container>
</section>
);
}
+86
View File
@@ -0,0 +1,86 @@
import Image from 'next/image';
import { Container } from '@/components/shared/container';
import { Marquee } from '@/components/ui/marquee';
const customerLogos = [
{
name: 'Next.js',
src: 'https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/nextjs-logo.svg',
width: 394,
height: 80,
},
{
name: 'OpenAI',
src: 'https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/openai-logo.svg',
width: 512,
height: 126,
},
{
name: 'Stripe',
src: 'https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/stripe-logo.svg',
width: 73,
height: 31,
},
{
name: 'Vercel',
src: 'https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/vercel-logo.svg',
width: 2048,
height: 407,
},
{
name: 'Supabase',
src: 'https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/supabase-logo.svg',
width: 581,
height: 113,
},
{
name: 'Anthropic',
src: 'https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/anthropic-logo.svg',
width: 579,
height: 65,
},
{
name: 'Tailwind CSS',
src: 'https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/tailwindcss-logo.svg',
width: 665,
height: 82,
},
{
name: 'Linear',
src: 'https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/linear-logo.svg',
width: 122,
height: 31,
},
];
export function LogosSection() {
return (
<section className="border-y border-border bg-foreground/[0.02]">
<Container className="py-12">
<p className="text-center text-xs font-medium uppercase tracking-[0.2em] text-foreground/40">
Trusted by fast-moving teams shipping to millions
</p>
<div className="relative mt-8 overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_15%,black_85%,transparent)]">
<Marquee pauseOnHover className="[--duration:40s] [--gap:4rem]">
{customerLogos.map((logo) => (
<div
key={logo.name}
className="group flex h-12 items-center justify-center px-2"
>
<Image
src={logo.src}
alt={`${logo.name} logo`}
width={logo.width}
height={logo.height}
unoptimized
className="h-7 w-[120px] max-w-full object-contain opacity-40 brightness-0 transition-opacity duration-200 group-hover:opacity-80 dark:invert"
/>
</div>
))}
</Marquee>
</div>
</Container>
</section>
);
}
+32
View File
@@ -0,0 +1,32 @@
import { StatsGlobe } from '@/components/sections/stats/stats-globe';
import { Container } from '@/components/shared/container';
const stats = [
{ value: '45ms', label: 'Median edge response, worldwide' },
{ value: '99.99%', label: 'Platform uptime over the last year' },
{ value: '35+', label: 'Frameworks supported out of the box' },
{ value: '1.2T', label: 'Requests served every month' },
];
export function StatsSection() {
return (
<section className="relative overflow-hidden border-y border-border bg-background py-16 sm:py-24">
<StatsGlobe />
<Container className="relative z-10 grid grid-cols-2 gap-px px-0 lg:grid-cols-4">
{stats.map((stat) => (
<div
key={stat.label}
className="px-6 py-12 text-center ring-1 ring-border"
>
<div className="text-4xl font-semibold tracking-tight sm:text-5xl">
{stat.value}
</div>
<div className="mx-auto mt-3 max-w-[16ch] text-sm text-foreground/45">
{stat.label}
</div>
</div>
))}
</Container>
</section>
);
}
+67
View File
@@ -0,0 +1,67 @@
'use client';
import { type COBEOptions } from 'cobe';
import { useTheme } from 'next-themes';
import { Globe } from '@/components/ui/globe';
// Rotation per frame. Default globe is 0.005; lower is slower, 0 stops it.
const GLOBE_SPEED = 0.003;
const markers: COBEOptions['markers'] = [
{ location: [37.7749, -122.4194], size: 0.08 },
{ location: [40.7128, -74.006], size: 0.06 },
{ location: [51.5072, -0.1276], size: 0.07 },
{ location: [48.8566, 2.3522], size: 0.05 },
{ location: [1.3521, 103.8198], size: 0.08 },
{ location: [35.6762, 139.6503], size: 0.06 },
{ location: [-33.8688, 151.2093], size: 0.05 },
{ location: [-23.5505, -46.6333], size: 0.06 },
];
const darkGlobeConfig: COBEOptions = {
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: 0,
theta: 0.22,
dark: 1,
diffuse: 1.2,
mapSamples: 20000,
mapBrightness: 5,
baseColor: [0.22, 0.22, 0.22],
markerColor: [1, 1, 1],
glowColor: [0.1, 0.1, 0.1],
markers,
};
const lightGlobeConfig: COBEOptions = {
...darkGlobeConfig,
dark: 0,
diffuse: 0.4,
mapBrightness: 1.2,
baseColor: [0.88, 0.88, 0.88],
markerColor: [0.1, 0.1, 0.1],
glowColor: [0.96, 0.96, 0.96],
};
export function StatsGlobe() {
const { resolvedTheme } = useTheme();
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden [mask-image:radial-gradient(ellipse_80%_110%_at_50%_90%,black_45%,transparent_100%)]"
>
<div className="absolute left-1/2 top-[6%] aspect-square w-[760px] -translate-x-1/2 opacity-90 sm:w-[1040px] lg:w-[1280px]">
<Globe
className="max-w-none"
config={resolvedTheme === 'light' ? lightGlobeConfig : darkGlobeConfig}
speed={GLOBE_SPEED}
/>
</div>
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-background/80 to-transparent" />
</div>
);
}