From 536bb8c872680d08862251f81d236175ef691ddc Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 21 Mar 2026 20:26:23 +0100 Subject: [PATCH] feat: add live demo dashboard to landing page hero --- app/page.tsx | 35 +-- components/marketing/LiveDemo.tsx | 418 ++++++++++++++++++++++++++++++ 2 files changed, 437 insertions(+), 16 deletions(-) create mode 100644 components/marketing/LiveDemo.tsx diff --git a/app/page.tsx b/app/page.tsx index fa1abc4..d3a0ca2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -15,6 +15,7 @@ import DeleteSiteModal from '@/components/sites/DeleteSiteModal' import { Button } from '@ciphera-net/ui' import { XIcon, GlobeIcon } from '@ciphera-net/ui' import { Cookie, ShieldCheck, Code, Lightning, ArrowRight, GithubLogo } from '@phosphor-icons/react' +import LiveDemo from '@/components/marketing/LiveDemo' import FeatureSections from '@/components/marketing/FeatureSections' import ComparisonCards from '@/components/marketing/ComparisonCards' import CTASection from '@/components/marketing/CTASection' @@ -150,19 +151,14 @@ export default function HomePage() { if (!user) { return ( <> - {/* HERO — full viewport */} -
- -
-
- -
- {/* H1 */} + {/* HERO — compact headline + live demo */} +
+
Analytics without the{' '} @@ -173,23 +169,21 @@ export default function HomePage() { - {/* Subtitle */} - Respect your users' privacy while getting the insights you need. + Respect your users' privacy while getting the insights you need. No cookies, no IP tracking, fully GDPR compliant. - {/* CTAs */} - {/* Trust badges */} Cookie-free | @@ -215,6 +208,16 @@ export default function HomePage() { Under 2KB
+ + {/* Live Dashboard Demo */} + + +
diff --git a/components/marketing/LiveDemo.tsx b/components/marketing/LiveDemo.tsx new file mode 100644 index 0000000..5ff53d6 --- /dev/null +++ b/components/marketing/LiveDemo.tsx @@ -0,0 +1,418 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' + +// ── Helpers ────────────────────────────────────────────────── + +function rand(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +function fmtDuration(seconds: number) { + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return `${m}m ${s}s` +} + +// Generate realistic hourly visitor counts (low at night, peak afternoon) +function generateHourlyPattern(): { hour: string; visitors: number; pageviews: number }[] { + const base = [ + 12, 8, 5, 4, 3, 4, 8, 18, 35, 52, 64, 72, + 78, 85, 88, 82, 74, 60, 48, 38, 30, 25, 20, 16, + ] + return base.map((v, i) => ({ + hour: `${String(i).padStart(2, '0')}:00`, + visitors: v + rand(-4, 4), + pageviews: Math.round(v * 2.8) + rand(-6, 6), + })) +} + +// ── Static panel data ──────────────────────────────────────── + +const topPages = [ + { label: '/blog/privacy', pct: 85 }, + { label: '/pricing', pct: 65 }, + { label: '/docs', pct: 45 }, + { label: '/about', pct: 30 }, + { label: '/integrations', pct: 20 }, +] + +const topReferrers = [ + { label: 'Google', pct: 40 }, + { label: 'Direct', pct: 25 }, + { label: 'Twitter', pct: 15 }, + { label: 'GitHub', pct: 12 }, + { label: 'Reddit', pct: 8 }, +] + +const locations = [ + { flag: '\u{1F1E8}\u{1F1ED}', name: 'Switzerland', pct: 30 }, + { flag: '\u{1F1E9}\u{1F1EA}', name: 'Germany', pct: 22 }, + { flag: '\u{1F1FA}\u{1F1F8}', name: 'USA', pct: 18 }, + { flag: '\u{1F1EB}\u{1F1F7}', name: 'France', pct: 15 }, + { flag: '\u{1F1EC}\u{1F1E7}', name: 'UK', pct: 15 }, +] + +const technology = [ + { label: 'Chrome', pct: 62 }, + { label: 'Firefox', pct: 18 }, + { label: 'Safari', pct: 15 }, + { label: 'Edge', pct: 5 }, +] + +const campaigns = [ + { label: 'newsletter', pct: 45 }, + { label: 'twitter', pct: 30 }, + { label: 'producthunt', pct: 25 }, +] + +// Generate heatmap data: 7 rows (Mon-Sun) x 24 cols (hours) +function generateHeatmap(): number[][] { + return Array.from({ length: 7 }, (_, day) => + Array.from({ length: 24 }, (_, hour) => { + const isWeekend = day >= 5 + const isNight = hour >= 1 && hour <= 5 + const isPeak = hour >= 9 && hour <= 17 + const isMorning = hour >= 7 && hour <= 9 + const isEvening = hour >= 17 && hour <= 21 + + if (isNight) return rand(0, 1) + if (isWeekend) { + if (isPeak) return rand(2, 4) + return rand(1, 3) + } + if (isPeak) return rand(5, 8) + if (isMorning || isEvening) return rand(3, 5) + return rand(1, 3) + }) + ) +} + +function heatmapOpacity(value: number): string { + if (value <= 1) return 'bg-brand-orange/[0.05]' + if (value <= 3) return 'bg-brand-orange/[0.2]' + if (value <= 5) return 'bg-brand-orange/[0.5]' + return 'bg-brand-orange/[0.8]' +} + +// ── SVG chart helpers ──────────────────────────────────────── + +function buildSmoothPath( + points: { x: number; y: number }[], + close: boolean +): string { + if (points.length < 2) return '' + const d: string[] = [`M ${points[0].x},${points[0].y}`] + + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[Math.max(i - 1, 0)] + const p1 = points[i] + const p2 = points[i + 1] + const p3 = points[Math.min(i + 2, points.length - 1)] + + const cp1x = p1.x + (p2.x - p0.x) / 6 + const cp1y = p1.y + (p2.y - p0.y) / 6 + const cp2x = p2.x - (p3.x - p1.x) / 6 + const cp2y = p2.y - (p3.y - p1.y) / 6 + + d.push(`C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`) + } + + if (close) { + const last = points[points.length - 1] + const first = points[0] + d.push(`L ${last.x},200 L ${first.x},200 Z`) + } + + return d.join(' ') +} + +// ── Component ──────────────────────────────────────────────── + +export default function LiveDemo() { + const [visitors, setVisitors] = useState(2847) + const [pageviews, setPageviews] = useState(8432) + const [bounceRate, setBounceRate] = useState(42) + const [avgDuration, setAvgDuration] = useState(154) + const [realtimeVisitors, setRealtimeVisitors] = useState(12) + const [chartData, setChartData] = useState(generateHourlyPattern) + const heatmap = useRef(generateHeatmap()) + + useEffect(() => { + const id = setInterval(() => { + setVisitors((v) => v + rand(1, 3)) + setPageviews((v) => v + rand(2, 5)) + setBounceRate(() => 38 + rand(0, 7)) + setAvgDuration(() => 130 + rand(0, 90)) + setRealtimeVisitors(() => 8 + rand(0, 7)) + setChartData((prev) => { + const next = [...prev] + const lastHourNum = + parseInt(next[next.length - 1].hour.split(':')[0], 10) + const newHour = (lastHourNum + 1) % 24 + next.push({ + hour: `${String(newHour).padStart(2, '0')}:00`, + visitors: rand(20, 90), + pageviews: rand(50, 250), + }) + if (next.length > 24) next.shift() + return next + }) + }, 2500) + return () => clearInterval(id) + }, []) + + // ── Chart SVG ────────────────────────────────────────────── + + const chartW = 800 + const chartH = 200 + const maxVisitors = Math.max(...chartData.map((d) => d.visitors), 1) + + const chartPoints = chartData.map((d, i) => ({ + x: (i / (chartData.length - 1)) * chartW, + y: chartH - (d.visitors / maxVisitors) * (chartH - 20) - 10, + })) + + const linePath = buildSmoothPath(chartPoints, false) + const areaPath = buildSmoothPath(chartPoints, true) + + // ── Stats config ─────────────────────────────────────────── + + const stats = [ + { + label: 'Visitors', + value: visitors.toLocaleString(), + change: '+12%', + positive: true, + }, + { + label: 'Pageviews', + value: pageviews.toLocaleString(), + change: '+8%', + positive: true, + }, + { + label: 'Bounce Rate', + value: `${bounceRate}%`, + change: '-3%', + positive: false, + }, + { + label: 'Avg. Duration', + value: fmtDuration(avgDuration), + change: '+15%', + positive: true, + }, + ] + + const xLabels = chartData + .map((d, i) => ({ label: d.hour, i })) + .filter((d) => parseInt(d.label.split(':')[0], 10) % 4 === 0) + + const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + + // ── Render ───────────────────────────────────────────────── + + return ( +
+ {/* Browser chrome */} +
+
+ + + +
+
+ pulse.ciphera.net/sites/demo +
+
+ + {/* Dashboard body */} +
+ {/* Header bar */} +
+
+ Ciphera + ciphera.net + + + {realtimeVisitors} current visitors + +
+ + Today + +
+ + {/* Stats bar */} +
+ {stats.map((s) => ( +
+
+ {s.label} +
+
{s.value}
+
+ {s.positive ? '\u2191' : '\u2193'} + {s.change.replace(/[+-]/, '')} +
+
+ ))} +
+ + {/* Chart */} +
+ + + + + + + + + + {/* X-axis labels */} + {xLabels.map(({ label, i }) => ( + + {label} + + ))} + +
+ + {/* Two-column panels */} +
+ {/* Top Pages */} + + + {/* Top Referrers */} + + + {/* Locations */} +
+

+ Locations +

+
+ {locations.map((loc) => ( +
+
+ {loc.flag} + + {loc.name} + + + {loc.pct}% + +
+
+
+
+
+ ))} +
+
+ + {/* Technology */} + + + {/* Campaigns */} + + + {/* Peak Hours */} +
+

+ Peak Hours +

+
+ {heatmap.current.map((row, dayIdx) => ( +
+ + {dayLabels[dayIdx]} + +
+ {row.map((val, hourIdx) => ( +
+ ))} +
+
+ ))} +
+
+
+
+ + {/* Bottom gradient fade */} +
+
+ ) +} + +// ── Panel Card sub-component ───────────────────────────────── + +function PanelCard({ + title, + items, +}: { + title: string + items: { label: string; pct: number }[] +}) { + return ( +
+

+ {title} +

+
+ {items.map((item) => ( +
+
+ {item.label} + {item.pct}% +
+
+
+
+
+ ))} +
+
+ ) +}