diff --git a/landing/components/sections/dashboard-mockup.tsx b/landing/components/sections/dashboard-mockup.tsx index 6715468..72aa924 100644 --- a/landing/components/sections/dashboard-mockup.tsx +++ b/landing/components/sections/dashboard-mockup.tsx @@ -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 }; @@ -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 = [ @@ -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('trends'); + return ( -
-
- {kpis.map((k) => ( -
-
{k.label}
-
- {k.prefix} - - {k.suffix} -
-
{k.delta}
-
- ))} +
+ {/* macOS-style chrome */} +
+
+ + + +
+
+ app.acme.io/dashboard +
+
-
-
-
Last 30 days
- - - - - - - - - - +
+ {/* KPI strip (always visible) */} +
+ {kpis.map((k) => ( +
+
{k.label}
+
+ {k.prefix} + + {k.suffix} +
+
{k.delta}
+
+ ))} +
+ + {/* Tab nav */} +
+ {tabs.map((t) => ( + + ))}
-
-
Funnel
-
- {funnel.map((f) => ( -
-
- {f.label} - {f.value}% -
-
-
+ + {active === 'trends' && ( + +
Active users · last 30 days
+ + + + + + + + + + +
+ )} + + {active === 'funnel' && ( + +
Activation funnel · last 30 days
+
+ {funnel.map((f) => ( +
+
+ {f.label} + {f.value}% +
+
+ +
+
+ ))}
-
- ))} -
+ + )} + + {active === 'retention' && ( + +
Weekly cohorts · retention %
+
+ {retention.flatMap((row, r) => + row.map((v, c) => ( +
+ {v < 0 ? '·' : v} +
+ )), + )} +
+
+ )} +
diff --git a/landing/components/sections/logo-cloud.tsx b/landing/components/sections/logo-cloud.tsx index e19b717..56a4326 100644 --- a/landing/components/sections/logo-cloud.tsx +++ b/landing/components/sections/logo-cloud.tsx @@ -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 (
-

+

+ {logoCloud.stats.map((s) => ( +
+
+ {s.prefix} + + {s.suffix} +
+
{s.label}
+
+ ))} +
+ +

{logoCloud.heading}

- + {logoCloud.logos.map((l) => (