diff --git a/apps/public/content/docs/dashboard/understand-the-overview.mdx b/apps/public/content/docs/dashboard/understand-the-overview.mdx index 9f477fe7..4ebff0f0 100644 --- a/apps/public/content/docs/dashboard/understand-the-overview.mdx +++ b/apps/public/content/docs/dashboard/understand-the-overview.mdx @@ -59,7 +59,7 @@ The trailing edge of the line (the current, incomplete interval) is shown as a d ## Insights -If you have configured [Insights](/features/insights) for your project, a scrollable row of insight cards appears below the chart. Each card shows a pre-configured metric with its current value and trend. Clicking a card applies that insight's filter to the entire overview page. Insights are optional—this section is hidden when none have been configured. +If you have configured insights for your project, a scrollable row of insight cards appears below the chart. Each card shows a pre-configured metric with its current value and trend. Clicking a card applies that insight's filter to the entire overview page. Insights are optional—this section is hidden when none have been configured. --- diff --git a/apps/public/src/app/(home)/_sections/analytics-insights.tsx b/apps/public/src/app/(home)/_sections/analytics-insights.tsx index 9f8f8e86..29533e14 100644 --- a/apps/public/src/app/(home)/_sections/analytics-insights.tsx +++ b/apps/public/src/app/(home)/_sections/analytics-insights.tsx @@ -1,56 +1,38 @@ -import { - BarChart3Icon, - ChevronRightIcon, - DollarSignIcon, - GlobeIcon, - PlayCircleIcon, -} from 'lucide-react'; +import { ChevronRightIcon } from 'lucide-react'; import Link from 'next/link'; import { FeatureCard } from '@/components/feature-card'; +import { NotificationsIllustration } from '@/components/illustrations/notifications'; import { ProductAnalyticsIllustration } from '@/components/illustrations/product-analytics'; +import { RetentionIllustration } from '@/components/illustrations/retention'; +import { SessionReplayIllustration } from '@/components/illustrations/session-replay'; import { WebAnalyticsIllustration } from '@/components/illustrations/web-analytics'; import { Section, SectionHeader } from '@/components/section'; -const features = [ +function wrap(child: React.ReactNode) { + return
An open-source web and product analytics platform that combines the diff --git a/apps/public/src/app/(home)/_sections/pricing.tsx b/apps/public/src/app/(home)/_sections/pricing.tsx index b6175ff1..10669f85 100644 --- a/apps/public/src/app/(home)/_sections/pricing.tsx +++ b/apps/public/src/app/(home)/_sections/pricing.tsx @@ -55,6 +55,9 @@ export function Pricing() {
++++ +++ + + + + {quote.quote} diff --git a/apps/public/src/app/(home)/page.tsx b/apps/public/src/app/(home)/page.tsx index 8467a710..ee2d0b34 100644 --- a/apps/public/src/app/(home)/page.tsx +++ b/apps/public/src/app/(home)/page.tsx @@ -1,5 +1,6 @@ import { AnalyticsInsights } from './_sections/analytics-insights'; import { Collaboration } from './_sections/collaboration'; +import { FeatureSpotlight } from './_sections/feature-spotlight'; import { CtaBanner } from './_sections/cta-banner'; import { DataPrivacy } from './_sections/data-privacy'; import { Faq } from './_sections/faq'; @@ -57,6 +58,7 @@ export default function HomePage() { + diff --git a/apps/public/src/components/eu-flag.tsx b/apps/public/src/components/eu-flag.tsx new file mode 100644 index 00000000..85c3da06 --- /dev/null +++ b/apps/public/src/components/eu-flag.tsx @@ -0,0 +1,37 @@ +function star(cx: number, cy: number, outerR: number, innerR: number) { + const pts: string[] = []; + for (let i = 0; i < 10; i++) { + const r = i % 2 === 0 ? outerR : innerR; + const angle = (i * Math.PI) / 5 - Math.PI / 2; + pts.push(`${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`); + } + return pts.join(' '); +} + +const STARS = Array.from({ length: 12 }, (_, i) => { + const angle = (i * 30 - 90) * (Math.PI / 180); + return { + x: 12 + 5 * Math.cos(angle), + y: 8 + 5 * Math.sin(angle), + }; +}); + +export function EuFlag({ className }: { className?: string }) { + return ( + + ); +} diff --git a/apps/public/src/components/illustrations/conversions.tsx b/apps/public/src/components/illustrations/conversions.tsx new file mode 100644 index 00000000..44db4fab --- /dev/null +++ b/apps/public/src/components/illustrations/conversions.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { SimpleChart } from '@/components/simple-chart'; + +const variantA = [28, 31, 29, 34, 32, 36, 35, 38, 37, 40, 39, 42]; +const variantB = [28, 30, 32, 35, 38, 37, 40, 42, 44, 43, 47, 50]; + +export function ConversionsIllustration() { + return ( + + {/* A/B variant cards */} ++ ); +} diff --git a/apps/public/src/components/illustrations/google-search-console.tsx b/apps/public/src/components/illustrations/google-search-console.tsx new file mode 100644 index 00000000..99c3ab1b --- /dev/null +++ b/apps/public/src/components/illustrations/google-search-console.tsx @@ -0,0 +1,78 @@ +const queries = [ + { + query: 'openpanel analytics', + clicks: 312, + impressions: '4.1k', + pos: 1.2, + }, + { + query: 'open source mixpanel alternative', + clicks: 187, + impressions: '3.8k', + pos: 2.4, + }, + { + query: 'web analytics without cookies', + clicks: 98, + impressions: '2.2k', + pos: 4.7, + }, +]; + +export function GoogleSearchConsoleIllustration() { + return ( +++ + {/* Breakdown label */} ++++ + Variant A + ++ 28.4% ++ +++ + Variant B ↑ + ++ + 41.2% + ++ + + Breakdown by experiment variant + +++++ ++ A: 57% ++++ ++ B: 82% ++ {/* Top stats */} ++ ); +} diff --git a/apps/public/src/components/illustrations/notifications.tsx b/apps/public/src/components/illustrations/notifications.tsx new file mode 100644 index 00000000..270cbaac --- /dev/null +++ b/apps/public/src/components/illustrations/notifications.tsx @@ -0,0 +1,47 @@ +import { CheckCircleIcon } from 'lucide-react'; + +export function NotificationsIllustration() { + return ( +++ + {/* Query table */} ++ + Clicks + + 740 +++ + Impr. + + 13k +++ + Avg. CTR + + 5.7% +++ + Avg. Pos + + 2.8 +++++ + Query + + + Pos + ++ {queries.map((q, i) => ( ++ {q.query} + + {q.pos} + ++ ))} ++ {/* Funnel completion notification */} ++ ); +} diff --git a/apps/public/src/components/illustrations/retention.tsx b/apps/public/src/components/illustrations/retention.tsx new file mode 100644 index 00000000..47a95916 --- /dev/null +++ b/apps/public/src/components/illustrations/retention.tsx @@ -0,0 +1,63 @@ +'use client'; + +const cohorts = [ + { label: 'Week 1', values: [100, 68, 45, 38, 31] }, + { label: 'Week 2', values: [100, 72, 51, 42, 35] }, + { label: 'Week 3', values: [100, 65, 48, 39, null] }, + { label: 'Week 4', values: [100, 70, null, null, null] }, +]; + +const headers = ['Day 0', 'Day 1', 'Day 7', 'Day 14', 'Day 30']; + +function cellStyle(v: number | null) { + if (v === null) { + return { + backgroundColor: 'transparent', + borderColor: 'var(--border)', + color: 'var(--muted-foreground)', + }; + } + const opacity = 0.12 + (v / 100) * 0.7; + return { + backgroundColor: `rgba(34, 197, 94, ${opacity})`, + borderColor: `rgba(34, 197, 94, 0.3)`, + color: v > 55 ? 'rgba(0,0,0,0.75)' : 'var(--foreground)', + }; +} + +export function RetentionIllustration() { + return ( +++ + {/* Notification rule */} ++++ Funnel completed + + just now + + Signup Flow — 142 today
++++ ++ + 71% conversion + ++ + Notification rule + +++ When + + Signup Flow + + completes → + + #growth + ++++ ); +} diff --git a/apps/public/src/components/illustrations/revenue.tsx b/apps/public/src/components/illustrations/revenue.tsx new file mode 100644 index 00000000..a6cc2b49 --- /dev/null +++ b/apps/public/src/components/illustrations/revenue.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { SimpleChart } from '@/components/simple-chart'; + +const revenuePoints = [28, 34, 31, 40, 37, 44, 41, 50, 47, 56, 59, 65]; + +const referrers = [ + { name: 'google.com', amount: '$3,840', pct: 46 }, + { name: 'twitter.com', amount: '$1,920', pct: 23 }, + { name: 'github.com', amount: '$1,260', pct: 15 }, + { name: 'direct', amount: '$1,400', pct: 16 }, +]; + +export function RevenueIllustration() { + return ( ++++ + {headers.map((h) => ( ++ {cohorts.map(({ label, values }) => ( ++ {h} ++ ))} +++ ))} ++ {label} ++ {values.map((v, i) => ( ++ {v !== null ? `${v}%` : '—'} ++ ))} ++ {/* MRR stat + chart */} ++ ); +} diff --git a/apps/public/src/components/illustrations/session-replay.tsx b/apps/public/src/components/illustrations/session-replay.tsx new file mode 100644 index 00000000..07c8e696 --- /dev/null +++ b/apps/public/src/components/illustrations/session-replay.tsx @@ -0,0 +1,89 @@ +import { PlayIcon } from 'lucide-react'; + +export function SessionReplayIllustration() { + return ( +++ + {/* Revenue by referrer */} ++ + MRR + + + $8,420 + + ↑ 12% this month +++ MRR over time +++ +++ + Referrer + + + Revenue + ++ {referrers.map((r) => ( ++ + {r.name} + ++ ))} ++ ++ + {r.amount} + +++ ); +} diff --git a/apps/public/src/components/illustrations/web-analytics.tsx b/apps/public/src/components/illustrations/web-analytics.tsx index 26d604f1..49651cb9 100644 --- a/apps/public/src/components/illustrations/web-analytics.tsx +++ b/apps/public/src/components/illustrations/web-analytics.tsx @@ -1,188 +1,165 @@ 'use client'; -import { SimpleChart } from '@/components/simple-chart'; -import { cn } from '@/lib/utils'; + import NumberFlow from '@number-flow/react'; -import { AnimatePresence, motion } from 'framer-motion'; -import { ArrowUpIcon } from 'lucide-react'; import Image from 'next/image'; import { useEffect, useState } from 'react'; -const TRAFFIC_SOURCES = [ +const VISITOR_DATA = [1840, 2100, 1950, 2400, 2250, 2650, 2980]; +const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + +const STATS = [ + { label: 'Visitors', value: 4128, formatted: null, change: 12, up: true }, + { label: 'Page views', value: 12438, formatted: '12.4k', change: 8, up: true }, + { label: 'Bounce rate', value: null, formatted: '42%', change: 3, up: false }, + { label: 'Avg. session', value: null, formatted: '3m 23s', change: 5, up: true }, +]; + +const SOURCES = [ { icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Fgoogle.com', - name: 'Google', - percentage: 49, - value: 2039, - }, - { - icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Finstagram.com', - name: 'Instagram', - percentage: 23, - value: 920, - }, - { - icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Ffacebook.com', - name: 'Facebook', - percentage: 18, - value: 750, + name: 'google.com', + pct: 49, }, { icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Ftwitter.com', - name: 'Twitter', - percentage: 10, - value: 412, + name: 'twitter.com', + pct: 21, + }, + { + icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Fgithub.com', + name: 'github.com', + pct: 14, }, ]; -const COUNTRIES = [ - { icon: '🇺🇸', name: 'United States', percentage: 37, value: 1842 }, - { icon: '🇩🇪', name: 'Germany', percentage: 28, value: 1391 }, - { icon: '🇬🇧', name: 'United Kingdom', percentage: 20, value: 982 }, - { icon: '🇯🇵', name: 'Japan', percentage: 15, value: 751 }, -]; +function AreaChart({ data }: { data: number[] }) { + const max = Math.max(...data); + const w = 400; + const h = 64; + const xStep = w / (data.length - 1); + const pts = data.map((v, i) => ({ x: i * xStep, y: h - (v / max) * h })); + const line = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' '); + const area = `${line} L ${w},${h} L 0,${h} Z`; + const last = pts[pts.length - 1]; + + return ( + + ); +} export function WebAnalyticsIllustration() { - const [currentSourceIndex, setCurrentSourceIndex] = useState(0); + const [liveVisitors, setLiveVisitors] = useState(47); useEffect(() => { - const interval = setInterval(() => { - setCurrentSourceIndex((prev) => (prev + 1) % TRAFFIC_SOURCES.length); - }, 3000); - - return () => clearInterval(interval); + const values = [47, 51, 44, 53, 49, 56]; + let i = 0; + const id = setInterval(() => { + i = (i + 1) % values.length; + setLiveVisitors(values[i]); + }, 2500); + return () => clearInterval(id); }, []); return ( -+ {/* Browser chrome */} +++ + + ++ + {/* Page content */} ++ app.example.com/pricing +++ + ++ + {/* Playback bar */} ++ + ++ + + + {/* Click heatspot */} ++ +++ ++ + {/* Cursor trail */} + + + {/* Cursor */} ++ +++++ + ++ + 0:52 / 2:05 + +-- ); -} - -function MetricCard({ - title, - value, - change, - chartPoints, - color, - className, -}: { - title: string; - value: string; - change: string; - chartPoints: number[]; - color: string; - className?: string; -}) { - return ( ---- - --- - -- ); -} - -function BarCell({ - icon, - name, - percentage, - value, - className, -}: { - icon: string; - name: string; - percentage: number; - value: number; - className?: string; -}) { - return ( -----{title}-{value}---- {change}-- - --- {icon.startsWith('http') ? ( --- ) : ( - {icon}- )} -- -- {name} - -- -% + + {/* Header */} +); diff --git a/apps/public/src/components/perks.tsx b/apps/public/src/components/perks.tsx index 5098b919..2f3140ff 100644 --- a/apps/public/src/components/perks.tsx +++ b/apps/public/src/components/perks.tsx @@ -1,10 +1,13 @@ +import type React from 'react'; import { cn } from '@/lib/utils'; import type { LucideIcon } from 'lucide-react'; +type PerkIcon = LucideIcon | React.ComponentType<{ className?: string }>; + export function Perks({ perks, className, -}: { perks: { text: string; icon: LucideIcon }[]; className?: string }) { +}: { perks: { text: string; icon: PerkIcon }[]; className?: string }) { return (++ + {/* KPI tiles */} ++ + + + + ++ + Last 7 days + +online now - + {STATS.map((stat) => ( ++ + {/* Area chart */} ++ {stat.label} + + {stat.formatted ?? + (stat.value !== null ? ( ++ ))} ++ ) : null)} + + + {stat.up ? '↑' : '↓'} {stat.change}% + + + Unique visitors ++ + {/* Traffic sources */} ++ + {DAYS.map((d) => ( + + {d} + + ))} +++ {SOURCES.map((src) => ( +++ ))}+ {src.name} + + {src.pct}% + + {perks.map((perk) => (