- {/* 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 */}
initiateOAuthFlow()} variant="primary" className="px-6 py-3 shadow-lg shadow-brand-orange/20 gap-2">
Try Pulse Free
@@ -199,12 +193,11 @@ export default function HomePage() {
- {/* 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}%
+
+
+
+ ))}
+
+
+ )
+}