Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 161 additions & 43 deletions landing/components/sections/dashboard-mockup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client';

import { useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { NumberTicker } from '@/components/magicui/number-ticker';
import { cn } from '@/lib/utils';

type Kpi = { label: string; value: number; prefix?: string; suffix?: string; delta: string };

Expand All @@ -23,7 +26,6 @@ const path = linePoints
return `${i === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${ny.toFixed(2)}`;
})
.join(' ');

const areaPath = `${path} L 100 100 L 0 100 Z`;

const funnel = [
Expand All @@ -33,55 +35,171 @@ const funnel = [
{ label: 'Paid', value: 18 },
];

// 8 weekly cohorts × 8 week buckets retention heatmap (synthetic but plausible)
const retention: number[][] = Array.from({ length: 8 }, (_, row) =>
Array.from({ length: 8 }, (_, col) => {
if (col === 0) return 100;
if (col > 7 - row) return -1; // not yet observed for newer cohorts
const base = 92 - col * 8 - row * 1.4;
return Math.max(18, Math.round(base));
}),
);

const tabs = [
{ id: 'trends', label: 'Trends' },
{ id: 'funnel', label: 'Funnel' },
{ id: 'retention', label: 'Retention' },
] as const;

type TabId = (typeof tabs)[number]['id'];

export function DashboardMockup() {
const [active, setActive] = useState<TabId>('trends');

return (
<div className="rounded-2xl border border-border bg-card/80 p-4 shadow-2xl backdrop-blur">
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
{kpis.map((k) => (
<div key={k.label} className="rounded-xl border border-border bg-background/60 p-3">
<div className="text-xs text-muted-foreground">{k.label}</div>
<div className="mt-1 flex items-baseline gap-1 text-xl font-semibold tracking-tight">
{k.prefix}
<NumberTicker value={k.value} decimalPlaces={k.suffix === '%' && k.value < 10 ? 1 : 0} />
{k.suffix}
</div>
<div className="mt-1 text-xs text-emerald-500">{k.delta}</div>
</div>
))}
<div className="overflow-hidden rounded-2xl border border-border bg-card/80 shadow-2xl backdrop-blur">
{/* macOS-style chrome */}
<div className="flex items-center gap-3 border-b border-border bg-secondary/40 px-4 py-2.5">
<div className="flex gap-1.5">
<span className="size-2.5 rounded-full bg-red-400/80" />
<span className="size-2.5 rounded-full bg-yellow-400/80" />
<span className="size-2.5 rounded-full bg-emerald-400/80" />
</div>
<div className="mx-auto flex h-6 max-w-xs flex-1 items-center justify-center rounded-md border border-border bg-background/60 px-3 text-[10px] text-muted-foreground">
app.acme.io/dashboard
</div>
<div className="w-9" aria-hidden />
</div>

<div className="mt-4 grid gap-3 md:grid-cols-[2fr_1fr]">
<div className="rounded-xl border border-border bg-background/60 p-4">
<div className="text-xs text-muted-foreground">Last 30 days</div>
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="mt-2 h-32 w-full">
<defs>
<linearGradient id="grad" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.35" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
<path d={areaPath} className="text-primary" fill="url(#grad)" />
<path d={path} className="text-primary" fill="none" stroke="currentColor" strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
</svg>
<div className="p-4">
{/* KPI strip (always visible) */}
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
{kpis.map((k) => (
<div key={k.label} className="rounded-xl border border-border bg-background/60 p-3">
<div className="text-xs text-muted-foreground">{k.label}</div>
<div className="mt-1 flex items-baseline gap-1 text-xl font-semibold tracking-tight">
{k.prefix}
<NumberTicker value={k.value} decimalPlaces={k.suffix === '%' && k.value < 10 ? 1 : 0} />
{k.suffix}
</div>
<div className="mt-1 text-xs text-emerald-500">{k.delta}</div>
</div>
))}
</div>

{/* Tab nav */}
<div className="mt-4 flex gap-1 rounded-lg border border-border bg-background/40 p-1 text-xs">
{tabs.map((t) => (
<button
key={t.id}
type="button"
onClick={() => setActive(t.id)}
className={cn(
'flex-1 rounded-md px-3 py-1.5 font-medium transition',
active === t.id
? 'bg-card text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground',
)}
>
{t.label}
</button>
))}
</div>
<div className="rounded-xl border border-border bg-background/60 p-4">
<div className="text-xs text-muted-foreground">Funnel</div>
<div className="mt-3 space-y-2">
{funnel.map((f) => (
<div key={f.label}>
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">{f.label}</span>
<span className="tabular-nums">{f.value}%</span>
</div>
<div className="mt-1 h-1.5 rounded-full bg-secondary">
<div
className="h-full rounded-full bg-primary"
style={{ width: `${f.value}%` }}

{/* Tab content */}
<div className="mt-3 min-h-[180px] rounded-xl border border-border bg-background/60 p-4">
<AnimatePresence mode="wait">
{active === 'trends' && (
<motion.div
key="trends"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
>
<div className="text-xs text-muted-foreground">Active users · last 30 days</div>
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="mt-2 h-36 w-full">
<defs>
<linearGradient id="grad" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.35" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
<path d={areaPath} className="text-primary" fill="url(#grad)" />
<path
d={path}
className="text-primary"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
vectorEffect="non-scaling-stroke"
/>
</svg>
</motion.div>
)}

{active === 'funnel' && (
<motion.div
key="funnel"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
>
<div className="text-xs text-muted-foreground">Activation funnel · last 30 days</div>
<div className="mt-3 space-y-3">
{funnel.map((f) => (
<div key={f.label}>
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">{f.label}</span>
<span className="tabular-nums">{f.value}%</span>
</div>
<div className="mt-1 h-2 rounded-full bg-secondary">
<motion.div
className="h-full rounded-full bg-primary"
initial={{ width: 0 }}
animate={{ width: `${f.value}%` }}
transition={{ duration: 0.5, ease: 'easeOut' }}
/>
</div>
</div>
))}
</div>
</div>
))}
</div>
</motion.div>
)}

{active === 'retention' && (
<motion.div
key="retention"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
>
<div className="text-xs text-muted-foreground">Weekly cohorts · retention %</div>
<div className="mt-3 grid gap-1" style={{ gridTemplateColumns: 'repeat(8, minmax(0, 1fr))' }}>
{retention.flatMap((row, r) =>
row.map((v, c) => (
<div
key={`${r}-${c}`}
className={cn(
'aspect-square rounded-sm text-[9px] flex items-center justify-center tabular-nums',
v < 0 ? 'bg-secondary/30 text-muted-foreground/40' : 'text-primary-foreground',
)}
style={
v < 0
? undefined
: { backgroundColor: `hsl(var(--primary) / ${0.15 + (v / 100) * 0.75})` }
}
>
{v < 0 ? '·' : v}
</div>
)),
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
Expand Down
18 changes: 16 additions & 2 deletions landing/components/sections/logo-cloud.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import Image from 'next/image';
import { Marquee } from '@/components/magicui/marquee';
import { NumberTicker } from '@/components/magicui/number-ticker';
import { logoCloud } from '@/lib/content';

export function LogoCloud() {
return (
<section className="border-b border-border py-14">
<div className="mx-auto max-w-6xl px-6">
<p className="text-center text-xs uppercase tracking-widest text-muted-foreground">
<dl className="grid gap-8 sm:grid-cols-3">
{logoCloud.stats.map((s) => (
<div key={s.label} className="text-center">
<dt className="flex items-baseline justify-center gap-0.5 text-4xl font-semibold tracking-tight sm:text-5xl">
{s.prefix}
<NumberTicker value={s.value} decimalPlaces={s.decimals ?? 0} />
{s.suffix}
</dt>
<dd className="mt-2 text-sm text-muted-foreground">{s.label}</dd>
</div>
))}
</dl>

<p className="mt-12 text-center text-xs uppercase tracking-widest text-muted-foreground">
{logoCloud.heading}
</p>
<Marquee pauseOnHover className="mt-8 [--duration:35s]">
<Marquee pauseOnHover className="mt-6 [--duration:35s]">
{logoCloud.logos.map((l) => (
<div key={l.name} className="mx-8 flex h-8 w-28 items-center justify-center">
<Image
Expand Down
13 changes: 12 additions & 1 deletion landing/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,19 @@ export const hero = {
secondaryCta: { label: 'View live demo', href: '#features' },
};

export const logoCloud = {
export type Stat = { value: number; prefix?: string; suffix?: string; decimals?: number; label: string };

export const logoCloud: {
heading: string;
stats: Stat[];
logos: { name: string; src: string }[];
} = {
heading: 'Trusted by teams shipping with Acme',
stats: [
{ value: 5000, suffix: '+', label: 'teams using Acme' },
{ value: 200, suffix: 'M', label: 'events ingested / week' },
{ value: 99.99, suffix: '%', decimals: 2, label: 'uptime over the last year' },
],
logos: [
{ name: 'GitHub', src: '/logos/github.svg' },
{ name: 'Vercel', src: '/logos/vercel.svg' },
Expand Down
4 changes: 2 additions & 2 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
},
{
"slug": "landing",
"name": "SaaS Landing",
"description": "Convert visitors before you've written a single API route. Dark-mode SaaS landing with pricing, FAQ, and a waitlist that actually stores signups.",
"name": "Launchpad",
"description": "Tell visitors what you're shipping before you've shipped it. Interactive product preview with tab-switching scenes, animated KPIs, pricing toggle, and a waitlist that writes straight to your database. Drop your copy in and launch.",
"category": "marketing",
"framework": "nextjs",
"features": ["Waitlist Capture", "Pricing Page", "shadcn/ui", "Auth"],
Expand Down
Loading