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
{child}
; +} + +const mediumFeatures = [ { - title: 'Revenue tracking', + title: 'Retention', description: - 'Track revenue from your payments and get insights into your revenue sources.', - icon: DollarSignIcon, - link: { - href: '/features/revenue-tracking', - children: 'More about revenue', - }, - }, - { - title: 'Profiles & Sessions', - description: - 'Track individual users and their complete journey across your platform.', - icon: GlobeIcon, - link: { - href: '/features/identify-users', - children: 'Identify your users', - }, - }, - { - title: 'Event Tracking', - description: - 'Capture every important interaction with flexible event tracking.', - icon: BarChart3Icon, - link: { - href: '/features/event-tracking', - children: 'All about tracking', - }, + 'Know how many users come back after day 1, day 7, day 30. Identify which behaviors predict long-term retention.', + illustration: wrap(), + link: { href: '/features/retention', children: 'View retention' }, }, { title: 'Session Replay', description: - 'Watch real user sessions to see exactly what happened. Privacy controls built in, loads async.', - icon: PlayCircleIcon, - link: { - href: '/features/session-replay', - children: 'See session replay', - }, + 'Watch real user sessions to see exactly what happened — clicks, scrolls, rage clicks. Privacy controls built in.', + illustration: wrap(), + link: { href: '/features/session-replay', children: 'See session replay' }, + }, + { + title: 'Notifications', + description: + 'Get notified when a funnel is completed. Stay on top of key moments in your product without watching dashboards all day.', + illustration: wrap(), + link: { href: '/features/notifications', children: 'Set up notifications' }, }, ]; @@ -59,37 +41,39 @@ export function AnalyticsInsights() {
+
} title="Web Analytics" - variant="large" /> } title="Product Analytics" - variant="large" />
-
- {features.map((feature) => ( + +
+ {mediumFeatures.map((feature) => ( ))}
+

} - title="Privacy-first" + title="GDPR compliant" variant="large" /> {child}
; +} + +const features = [ + { + title: 'Revenue Tracking', + description: + 'Connect payment events to track MRR and see which referrers drive the most revenue.', + illustration: wrap(), + link: { + href: '/features/revenue-tracking', + children: 'Track revenue', + }, + }, + { + title: 'Conversion Tracking', + description: + 'Monitor conversion rates over time and break down by A/B variant, country, or device. Catch regressions before they cost you.', + illustration: wrap(), + link: { + href: '/features/conversion', + children: 'Track conversions', + }, + }, + { + title: 'Google Search Console', + description: + 'See which search queries bring organic traffic and how visitors convert after landing. Your SEO and product data, in one place.', + illustration: wrap(), + link: { + href: '/features/integrations', + children: 'View integrations', + }, + }, +]; + +export function FeatureSpotlight() { + return ( +
+ + +
+ {features.map((feature) => ( + + ))} +
+
+ ); +} diff --git a/apps/public/src/app/(home)/_sections/hero.tsx b/apps/public/src/app/(home)/_sections/hero.tsx index a76e8954..50c38e7d 100644 --- a/apps/public/src/app/(home)/_sections/hero.tsx +++ b/apps/public/src/app/(home)/_sections/hero.tsx @@ -5,14 +5,14 @@ import { CalendarIcon, CookieIcon, CreditCardIcon, - DatabaseIcon, GithubIcon, - ServerIcon, + ShieldCheckIcon, } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; import { useState } from 'react'; import { Competition } from '@/components/competition'; +import { EuFlag } from '@/components/eu-flag'; import { GetStartedButton } from '@/components/get-started-button'; import { Perks } from '@/components/perks'; import { Button } from '@/components/ui/button'; @@ -21,10 +21,10 @@ import { cn } from '@/lib/utils'; const perks = [ { text: 'Free trial 30 days', icon: CalendarIcon }, { text: 'No credit card required', icon: CreditCardIcon }, + { text: 'GDPR compliant', icon: ShieldCheckIcon }, + { text: 'EU hosted', icon: EuFlag }, { text: 'Cookie-less tracking', icon: CookieIcon }, { text: 'Open-source', icon: GithubIcon }, - { text: 'Your data, your rules', icon: DatabaseIcon }, - { text: 'Self-hostable', icon: ServerIcon }, ]; const aspectRatio = 2946 / 1329; @@ -90,7 +90,7 @@ export function Hero() { TRUSTED BY 1,000+ PROJECTS

- OpenPanel - The open-source alternative to + The open-source alternative to

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() {

{selected ? ( <> + + 30-day free trial +
- - 30-day free trial -
diff --git a/apps/public/src/app/(home)/_sections/why-openpanel.tsx b/apps/public/src/app/(home)/_sections/why-openpanel.tsx index d10d4e59..e6da3d1c 100644 --- a/apps/public/src/app/(home)/_sections/why-openpanel.tsx +++ b/apps/public/src/app/(home)/_sections/why-openpanel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { QuoteIcon } from 'lucide-react'; +import { QuoteIcon, StarIcon } from 'lucide-react'; import Image from 'next/image'; import Markdown from 'react-markdown'; import { FeatureCardBackground } from '@/components/feature-card'; @@ -94,13 +94,22 @@ export function WhyOpenPanel() { ))}
- {quotes.map((quote) => ( + {quotes.slice(0, 2).map((quote) => (
- -
+
+ +
+ + + + + +
+
+
{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 ( + + + {STARS.map((s, i) => ( + + ))} + + ); +} 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 */} +
+
+
+ + Variant A + +
+ 28.4% + +
+
+
+ + Variant B ↑ + +
+ + 41.2% + + +
+
+ + {/* Breakdown label */} +
+ + Breakdown by experiment variant + +
+
+
+
+ A: 57% +
+
+
+
+
+ B: 82% +
+
+
+ ); +} 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 ( +
+ {/* Top stats */} +
+
+ + Clicks + + 740 +
+
+ + Impr. + + 13k +
+
+ + Avg. CTR + + 5.7% +
+
+ + Avg. Pos + + 2.8 +
+
+ + {/* Query table */} +
+
+ + Query + + + Pos + +
+ {queries.map((q, i) => ( +
+ {q.query} + + {q.pos} + +
+ ))} +
+
+ ); +} 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 ( +
+ {/* Funnel completion notification */} +
+
+ + Funnel completed + + just now + +
+

Signup Flow — 142 today

+
+
+
+
+ + 71% conversion + +
+
+ + {/* Notification rule */} +
+ + Notification rule + +
+ When + + Signup Flow + + completes → + + #growth + +
+
+
+ ); +} 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 ( +
+
+
+
+ {headers.map((h) => ( +
+ {h} +
+ ))} +
+ {cohorts.map(({ label, values }) => ( +
+
+ {label} +
+ {values.map((v, i) => ( +
+ {v !== null ? `${v}%` : '—'} +
+ ))} +
+ ))} +
+
+ ); +} 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 ( +
+ {/* MRR stat + chart */} +
+
+ + MRR + + + $8,420 + + ↑ 12% this month +
+
+ MRR over time + +
+
+ + {/* Revenue by referrer */} +
+
+ + Referrer + + + Revenue + +
+ {referrers.map((r) => ( +
+ + {r.name} + +
+
+
+ + {r.amount} + +
+ ))} +
+
+ ); +} 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 ( +
+
+ {/* Browser chrome */} +
+
+
+
+
+ app.example.com/pricing +
+
+ + {/* Page content */} +
+
+
+
+
+
+
+
+
+ + {/* Click heatspot */} +
+
+
+
+
+
+ + {/* Cursor trail */} + + + + + {/* Cursor */} +
+ + + +
+
+ + {/* Playback bar */} +
+ +
+
+
+ + 0:52 / 2:05 + +
+
+
+ ); +} 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 ( -
-
- - -
- - -
-
-
- ); -} - -function MetricCard({ - title, - value, - change, - chartPoints, - color, - className, -}: { - title: string; - value: string; - change: string; - chartPoints: number[]; - color: string; - className?: string; -}) { - return ( -
-
-
-
{title}
-
{value}
-
-
- -
{change}
-
-
- -
- ); -} - -function BarCell({ - icon, - name, - percentage, - value, - className, -}: { - icon: string; - name: string; - percentage: number; - value: number; - className?: string; -}) { - return ( -
-
-
-
- {icon.startsWith('http') ? ( - serie icon - ) : ( -
{icon}
- )} - - - {name} - - -
-
- - % +
+ {/* Header */} +
+
+ + + + + + online now -
+ + Last 7 days + +
+ + {/* KPI tiles */} +
+ {STATS.map((stat) => ( +
+ {stat.label} + + {stat.formatted ?? + (stat.value !== null ? ( + + ) : null)} + + + {stat.up ? '↑' : '↓'} {stat.change}% + +
+ ))} +
+ + {/* Area chart */} +
+ Unique visitors + +
+ {DAYS.map((d) => ( + + {d} + + ))} +
+
+ + {/* Traffic sources */} +
+ {SOURCES.map((src) => ( +
+ {src.name} + {src.name} + + {src.pct}% + +
+ ))}
); 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 (
    {perks.map((perk) => (