From ca199b59fdb6f76d79375a7a40cca101d5028303 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 21 Mar 2026 20:36:55 +0100 Subject: [PATCH] feat: replace fake LiveDemo with real dashboard components and fake data --- app/page.tsx | 4 +- components/marketing/DashboardDemo.tsx | 283 +++++++++++++++++ components/marketing/LiveDemo.tsx | 418 ------------------------- 3 files changed, 285 insertions(+), 420 deletions(-) create mode 100644 components/marketing/DashboardDemo.tsx delete mode 100644 components/marketing/LiveDemo.tsx diff --git a/app/page.tsx b/app/page.tsx index d3a0ca2..851f089 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -15,7 +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 DashboardDemo from '@/components/marketing/DashboardDemo' import FeatureSections from '@/components/marketing/FeatureSections' import ComparisonCards from '@/components/marketing/ComparisonCards' import CTASection from '@/components/marketing/CTASection' @@ -216,7 +216,7 @@ export default function HomePage() { transition={{ duration: 0.7, delay: 0.4 }} className="w-full max-w-7xl mx-auto px-6" > - + diff --git a/components/marketing/DashboardDemo.tsx b/components/marketing/DashboardDemo.tsx new file mode 100644 index 0000000..d123519 --- /dev/null +++ b/components/marketing/DashboardDemo.tsx @@ -0,0 +1,283 @@ +'use client' + +import Chart from '@/components/dashboard/Chart' +import ContentStats from '@/components/dashboard/ContentStats' +import TopReferrers from '@/components/dashboard/TopReferrers' +import Locations from '@/components/dashboard/Locations' +import TechSpecs from '@/components/dashboard/TechSpecs' +import { useState } from 'react' + +// ─── Fake Data ─────────────────────────────────────────────────────── + +const FAKE_STATS = { pageviews: 8432, visitors: 2847, bounce_rate: 42, avg_duration: 154 } +const FAKE_PREV_STATS = { pageviews: 7821, visitors: 2543, bounce_rate: 45, avg_duration: 134 } + +const FAKE_DAILY_STATS = [ + { date: '2026-03-21 00:00:00', pageviews: 12, visitors: 8, bounce_rate: 45, avg_duration: 120 }, + { date: '2026-03-21 01:00:00', pageviews: 8, visitors: 5, bounce_rate: 50, avg_duration: 98 }, + { date: '2026-03-21 02:00:00', pageviews: 5, visitors: 3, bounce_rate: 55, avg_duration: 85 }, + { date: '2026-03-21 03:00:00', pageviews: 3, visitors: 2, bounce_rate: 60, avg_duration: 72 }, + { date: '2026-03-21 04:00:00', pageviews: 4, visitors: 3, bounce_rate: 58, avg_duration: 80 }, + { date: '2026-03-21 05:00:00', pageviews: 18, visitors: 12, bounce_rate: 48, avg_duration: 110 }, + { date: '2026-03-21 06:00:00', pageviews: 45, visitors: 32, bounce_rate: 44, avg_duration: 125 }, + { date: '2026-03-21 07:00:00', pageviews: 98, visitors: 67, bounce_rate: 40, avg_duration: 140 }, + { date: '2026-03-21 08:00:00', pageviews: 234, visitors: 156, bounce_rate: 38, avg_duration: 158 }, + { date: '2026-03-21 09:00:00', pageviews: 412, visitors: 278, bounce_rate: 36, avg_duration: 172 }, + { date: '2026-03-21 10:00:00', pageviews: 523, visitors: 342, bounce_rate: 35, avg_duration: 180 }, + { date: '2026-03-21 11:00:00', pageviews: 578, visitors: 367, bounce_rate: 37, avg_duration: 175 }, + { date: '2026-03-21 12:00:00', pageviews: 498, visitors: 312, bounce_rate: 40, avg_duration: 162 }, + { date: '2026-03-21 13:00:00', pageviews: 612, visitors: 398, bounce_rate: 34, avg_duration: 185 }, + { date: '2026-03-21 14:00:00', pageviews: 687, visitors: 445, bounce_rate: 32, avg_duration: 192 }, + { date: '2026-03-21 15:00:00', pageviews: 721, visitors: 468, bounce_rate: 31, avg_duration: 198 }, + { date: '2026-03-21 16:00:00', pageviews: 654, visitors: 423, bounce_rate: 33, avg_duration: 188 }, + { date: '2026-03-21 17:00:00', pageviews: 543, visitors: 356, bounce_rate: 36, avg_duration: 170 }, + { date: '2026-03-21 18:00:00', pageviews: 432, visitors: 287, bounce_rate: 39, avg_duration: 156 }, + { date: '2026-03-21 19:00:00', pageviews: 367, visitors: 245, bounce_rate: 41, avg_duration: 148 }, + { date: '2026-03-21 20:00:00', pageviews: 298, visitors: 198, bounce_rate: 43, avg_duration: 138 }, + { date: '2026-03-21 21:00:00', pageviews: 234, visitors: 156, bounce_rate: 44, avg_duration: 130 }, + { date: '2026-03-21 22:00:00', pageviews: 167, visitors: 112, bounce_rate: 46, avg_duration: 122 }, + { date: '2026-03-21 23:00:00', pageviews: 89, visitors: 58, bounce_rate: 48, avg_duration: 115 }, +] + +const FAKE_TOP_PAGES = [ + { path: '/', pageviews: 2341, visits: 1892 }, + { path: '/products/pulse', pageviews: 1567, visits: 1234 }, + { path: '/products/drop', pageviews: 987, visits: 812 }, + { path: '/pricing', pageviews: 876, visits: 723 }, + { path: '/blog/privacy-first-analytics', pageviews: 654, visits: 543 }, + { path: '/about', pageviews: 432, visits: 367 }, + { path: '/docs/getting-started', pageviews: 389, visits: 312 }, + { path: '/blog/end-to-end-encryption', pageviews: 345, visits: 289 }, + { path: '/contact', pageviews: 287, visits: 234 }, + { path: '/careers', pageviews: 198, visits: 167 }, +] + +const FAKE_ENTRY_PAGES = [ + { path: '/', pageviews: 1987, visits: 1654 }, + { path: '/products/pulse', pageviews: 1123, visits: 987 }, + { path: '/blog/privacy-first-analytics', pageviews: 567, visits: 489 }, + { path: '/products/drop', pageviews: 534, visits: 456 }, + { path: '/pricing', pageviews: 423, visits: 378 }, + { path: '/docs/getting-started', pageviews: 312, visits: 267 }, + { path: '/about', pageviews: 234, visits: 198 }, + { path: '/blog/end-to-end-encryption', pageviews: 198, visits: 167 }, + { path: '/careers', pageviews: 145, visits: 123 }, + { path: '/contact', pageviews: 112, visits: 98 }, +] + +const FAKE_EXIT_PAGES = [ + { path: '/pricing', pageviews: 1456, visits: 1234 }, + { path: '/', pageviews: 1234, visits: 1087 }, + { path: '/contact', pageviews: 876, visits: 756 }, + { path: '/products/drop', pageviews: 654, visits: 543 }, + { path: '/products/pulse', pageviews: 567, visits: 478 }, + { path: '/docs/getting-started', pageviews: 432, visits: 367 }, + { path: '/about', pageviews: 345, visits: 289 }, + { path: '/blog/privacy-first-analytics', pageviews: 298, visits: 245 }, + { path: '/careers', pageviews: 234, visits: 198 }, + { path: '/blog/end-to-end-encryption', pageviews: 178, visits: 145 }, +] + +const FAKE_REFERRERS = [ + { referrer: 'google.com', pageviews: 3421 }, + { referrer: '(direct)', pageviews: 2100 }, + { referrer: 'twitter.com', pageviews: 876 }, + { referrer: 'github.com', pageviews: 654 }, + { referrer: 'reddit.com', pageviews: 432 }, + { referrer: 'producthunt.com', pageviews: 312 }, + { referrer: 'news.ycombinator.com', pageviews: 267 }, + { referrer: 'linkedin.com', pageviews: 198 }, + { referrer: 'duckduckgo.com', pageviews: 112 }, + { referrer: 'dev.to', pageviews: 78 }, +] + +const FAKE_COUNTRIES = [ + { country: 'CH', pageviews: 2534 }, + { country: 'DE', pageviews: 1856 }, + { country: 'US', pageviews: 1234 }, + { country: 'FR', pageviews: 876 }, + { country: 'GB', pageviews: 654 }, + { country: 'NL', pageviews: 432 }, + { country: 'AT', pageviews: 312 }, + { country: 'SE', pageviews: 198 }, + { country: 'JP', pageviews: 156 }, + { country: 'CA', pageviews: 134 }, +] + +const FAKE_CITIES = [ + { city: 'Zurich', country: 'CH', pageviews: 1234 }, + { city: 'Geneva', country: 'CH', pageviews: 678 }, + { city: 'Berlin', country: 'DE', pageviews: 567 }, + { city: 'Munich', country: 'DE', pageviews: 432 }, + { city: 'San Francisco', country: 'US', pageviews: 345 }, + { city: 'Paris', country: 'FR', pageviews: 312 }, + { city: 'London', country: 'GB', pageviews: 289 }, + { city: 'Amsterdam', country: 'NL', pageviews: 234 }, + { city: 'Vienna', country: 'AT', pageviews: 198 }, + { city: 'New York', country: 'US', pageviews: 178 }, +] + +const FAKE_REGIONS = [ + { region: 'Zurich', country: 'CH', pageviews: 1567 }, + { region: 'Geneva', country: 'CH', pageviews: 734 }, + { region: 'Bavaria', country: 'DE', pageviews: 523 }, + { region: 'Berlin', country: 'DE', pageviews: 489 }, + { region: 'California', country: 'US', pageviews: 456 }, + { region: 'Ile-de-France', country: 'FR', pageviews: 345 }, + { region: 'England', country: 'GB', pageviews: 312 }, + { region: 'North Holland', country: 'NL', pageviews: 267 }, + { region: 'Bern', country: 'CH', pageviews: 234 }, + { region: 'New York', country: 'US', pageviews: 198 }, +] + +const FAKE_BROWSERS = [ + { browser: 'Chrome', pageviews: 5234 }, + { browser: 'Firefox', pageviews: 1518 }, + { browser: 'Safari', pageviews: 987 }, + { browser: 'Edge', pageviews: 456 }, + { browser: 'Brave', pageviews: 178 }, + { browser: 'Arc', pageviews: 59 }, +] + +const FAKE_OS = [ + { os: 'macOS', pageviews: 3421 }, + { os: 'Windows', pageviews: 2567 }, + { os: 'Linux', pageviews: 1234 }, + { os: 'iOS', pageviews: 756 }, + { os: 'Android', pageviews: 454 }, +] + +const FAKE_DEVICES = [ + { device: 'Desktop', pageviews: 5876 }, + { device: 'Mobile', pageviews: 1987 }, + { device: 'Tablet', pageviews: 569 }, +] + +const FAKE_SCREEN_RESOLUTIONS = [ + { screen_resolution: '1920x1080', pageviews: 2345 }, + { screen_resolution: '1440x900', pageviews: 1567 }, + { screen_resolution: '2560x1440', pageviews: 1234 }, + { screen_resolution: '1366x768', pageviews: 876 }, + { screen_resolution: '3840x2160', pageviews: 654 }, + { screen_resolution: '1536x864', pageviews: 432 }, + { screen_resolution: '390x844', pageviews: 312 }, + { screen_resolution: '393x873', pageviews: 234 }, +] + +// ─── Component ─────────────────────────────────────────────────────── + +export default function DashboardDemo() { + const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>('hour') + const [multiDayInterval, setMultiDayInterval] = useState<'hour' | 'day'>('day') + const today = new Date().toISOString().split('T')[0] + const dateRange = { start: today, end: today } + + const noop = () => {} + + return ( +
+ {/* Browser chrome */} +
+
+
+
+
+
+
+
+ pulse.ciphera.net/sites/demo +
+
+
+ + {/* Dashboard content */} +
+
+ {/* Dashboard header */} +
+
+
+

Ciphera

+

ciphera.net

+
+
+ + + + + 12 current visitors +
+
+
+ Today +
+
+ + {/* Chart with stats */} +
+ +
+ + {/* 2-col grid: Pages + Referrers */} +
+ + +
+ + {/* 2-col grid: Locations + Tech */} +
+ + +
+
+ + {/* Bottom gradient fade */} +
+
+
+ ) +} diff --git a/components/marketing/LiveDemo.tsx b/components/marketing/LiveDemo.tsx deleted file mode 100644 index 5ff53d6..0000000 --- a/components/marketing/LiveDemo.tsx +++ /dev/null @@ -1,418 +0,0 @@ -'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}% -
-
-
-
-
- ))} -
-
- ) -}