public: new page and copy improvements
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import {
|
||||
BarChart3Icon,
|
||||
ChevronRightIcon,
|
||||
@@ -7,8 +5,10 @@ import {
|
||||
GlobeIcon,
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { ProductAnalyticsIllustration } from './illustrations/product-analytics';
|
||||
import { WebAnalyticsIllustration } from './illustrations/web-analytics';
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { ProductAnalyticsIllustration } from '@/components/illustrations/product-analytics';
|
||||
import { WebAnalyticsIllustration } from '@/components/illustrations/web-analytics';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
|
||||
const features = [
|
||||
{
|
||||
@@ -47,42 +47,42 @@ export function AnalyticsInsights() {
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
description="Combine web and product analytics in one platform. Track visitors, events, revenue, and user journeys, all with privacy-first tracking."
|
||||
label="ANALYTICS & INSIGHTS"
|
||||
title="See the full picture of your users and product performance"
|
||||
description="Combine web and product analytics in one platform. Track visitors, events, revenue, and user journeys, all with privacy-first tracking."
|
||||
className="mb-16"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div className="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<FeatureCard
|
||||
variant="large"
|
||||
title="Web Analytics"
|
||||
className="px-0 **:data-content:px-6"
|
||||
description="Understand your website performance with privacy-first analytics and clear, actionable insights."
|
||||
illustration={<WebAnalyticsIllustration />}
|
||||
className="px-0 **:data-content:px-6"
|
||||
title="Web Analytics"
|
||||
variant="large"
|
||||
/>
|
||||
<FeatureCard
|
||||
variant="large"
|
||||
title="Product Analytics"
|
||||
className="px-0 **:data-content:px-6"
|
||||
description="Turn raw data into clarity with real-time visualization of performance, behavior, and trends."
|
||||
illustration={<ProductAnalyticsIllustration />}
|
||||
className="px-0 **:data-content:px-6"
|
||||
title="Product Analytics"
|
||||
variant="large"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{features.map((feature) => (
|
||||
<FeatureCard
|
||||
key={feature.title}
|
||||
title={feature.title}
|
||||
description={feature.description}
|
||||
icon={feature.icon}
|
||||
key={feature.title}
|
||||
link={feature.link}
|
||||
title={feature.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-8 text-center">
|
||||
<Link
|
||||
className="inline-flex items-center gap-1 text-muted-foreground text-sm transition-colors hover:text-foreground"
|
||||
href="/features"
|
||||
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1 transition-colors"
|
||||
>
|
||||
Explore all features
|
||||
<ChevronRightIcon className="size-3.5" />
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { BoltIcon, GithubIcon, ServerIcon } from 'lucide-react';
|
||||
import { DataOwnershipIllustration } from './illustrations/data-ownership';
|
||||
import { PrivacyIllustration } from './illustrations/privacy';
|
||||
import Link from 'next/link';
|
||||
import { FeatureCard } from '@/components/feature-card';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { DataOwnershipIllustration } from '@/components/illustrations/data-ownership';
|
||||
import { PrivacyIllustration } from '@/components/illustrations/privacy';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const secondaryFeatures = [
|
||||
{
|
||||
@@ -29,6 +32,7 @@ export function DataPrivacy() {
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
description="OpenPanel gives you analytics on your terms - privacy-friendly, open-source, and fully self-hostable. Every part of the platform is designed to put you in control of your data while delivering fast, reliable insights without compromising user trust."
|
||||
title={
|
||||
<>
|
||||
Built for Control,
|
||||
@@ -36,32 +40,37 @@ export function DataPrivacy() {
|
||||
Transparency & Trust
|
||||
</>
|
||||
}
|
||||
description="OpenPanel gives you analytics on your terms - privacy-friendly, open-source, and fully self-hostable. Every part of the platform is designed to put you in control of your data while delivering fast, reliable insights without compromising user trust."
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6 mt-16">
|
||||
<div className="mt-16 mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<FeatureCard
|
||||
variant="large"
|
||||
title="Privacy-first"
|
||||
description="Privacy-first analytics without cookies, fingerprinting, or invasive tracking. Built for compliance and user trust."
|
||||
illustration={<PrivacyIllustration />}
|
||||
title="Privacy-first"
|
||||
variant="large"
|
||||
/>
|
||||
<FeatureCard
|
||||
variant="large"
|
||||
title="Data Ownership"
|
||||
description="You own your data - no vendors, no sharing, no hidden processing. Store analytics on your own infrastructure and stay in full control."
|
||||
illustration={<DataOwnershipIllustration />}
|
||||
title="Data Ownership"
|
||||
variant="large"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{secondaryFeatures.map((feature) => (
|
||||
<FeatureCard
|
||||
key={feature.title}
|
||||
title={feature.title}
|
||||
description={feature.description}
|
||||
icon={feature.icon}
|
||||
key={feature.title}
|
||||
title={feature.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="row mt-8 gap-4">
|
||||
<GetStartedButton />
|
||||
<Button asChild className="px-6" size="lg" variant="outline">
|
||||
<Link href="/docs/self-hosting/self-hosting">Self-host for free</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FaqItem, Faqs } from '@/components/faq';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
|
||||
const faqData = [
|
||||
@@ -63,11 +64,13 @@ export function Faq() {
|
||||
return (
|
||||
<Section className="container">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
description="Some of the most common questions we get asked."
|
||||
title="FAQ"
|
||||
/>
|
||||
<div className="col gap-8">
|
||||
<SectionHeader
|
||||
description="Some of the most common questions we get asked."
|
||||
title="FAQ"
|
||||
/>
|
||||
<GetStartedButton className="w-fit max-md:hidden" />
|
||||
</div>
|
||||
<Faqs>
|
||||
{faqData.map((faq) => (
|
||||
<FaqItem key={faq.question} question={faq.question}>
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
'use client';
|
||||
import { Competition } from '@/components/competition';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Perks } from '@/components/perks';
|
||||
import { Tag } from '@/components/tag';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
CalendarIcon,
|
||||
ChevronRightIcon,
|
||||
CookieIcon,
|
||||
CreditCardIcon,
|
||||
DatabaseIcon,
|
||||
FlaskRoundIcon,
|
||||
GithubIcon,
|
||||
ServerIcon,
|
||||
StarIcon,
|
||||
} from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { Competition } from '@/components/competition';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Perks } from '@/components/perks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const perks = [
|
||||
{ text: 'Free trial 30 days', icon: CalendarIcon },
|
||||
@@ -40,46 +36,46 @@ function HeroImage({ className }: { className?: string }) {
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, x: 0 }}
|
||||
animate={
|
||||
isLoaded
|
||||
? { opacity: 0.5, scale: 1, x: 0 }
|
||||
: { opacity: 0, scale: 0.9, x: 0 }
|
||||
}
|
||||
transition={{
|
||||
duration: 2,
|
||||
}}
|
||||
className={cn('absolute', className)}
|
||||
initial={{ opacity: 0, scale: 0.9, x: 0 }}
|
||||
style={{
|
||||
left: `calc(50% - ${width / 2}px - 50px)`,
|
||||
top: -270,
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/hero-dark.webp"
|
||||
alt="Hero"
|
||||
width={width}
|
||||
height={height}
|
||||
className="hidden dark:block"
|
||||
height={height}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
src="/hero-dark.webp"
|
||||
style={{
|
||||
width,
|
||||
minWidth: width,
|
||||
height,
|
||||
}}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
width={width}
|
||||
/>
|
||||
<Image
|
||||
src="/hero-light.webp"
|
||||
alt="Hero"
|
||||
width={width}
|
||||
height={height}
|
||||
className="dark:hidden"
|
||||
height={height}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
src="/hero-light.webp"
|
||||
style={{
|
||||
width,
|
||||
minWidth: width,
|
||||
height,
|
||||
}}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
width={width}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
@@ -88,12 +84,12 @@ function HeroImage({ className }: { className?: string }) {
|
||||
export function Hero() {
|
||||
return (
|
||||
<HeroContainer className="-mb-32 max-sm:**:data-children:pb-0">
|
||||
<div className="col gap-8 w-full sm:w-1/2 sm:pr-12">
|
||||
<div className="col w-full gap-8 sm:w-1/2 sm:pr-12">
|
||||
<div className="col gap-4">
|
||||
{/* <div className="font-mono text-sm text-muted-foreground">
|
||||
TRUSTED BY 1,000+ COMPANIES • 4.7K GITHUB STARS
|
||||
</div> */}
|
||||
<h1 className="text-4xl md:text-5xl font-semibold leading-[1.1]">
|
||||
<div className="font-mono text-muted-foreground text-sm">
|
||||
TRUSTED BY 1,000+ PROJECTS
|
||||
</div>
|
||||
<h1 className="font-semibold text-4xl leading-[1.1] md:text-5xl">
|
||||
OpenPanel - The open-source alternative to <Competition />
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
@@ -104,7 +100,7 @@ export function Hero() {
|
||||
</div>
|
||||
<div className="row gap-4">
|
||||
<GetStartedButton />
|
||||
<Button size="lg" variant="outline" asChild className="px-6">
|
||||
<Button asChild className="px-6" size="lg" variant="outline">
|
||||
<Link
|
||||
href="https://demo.openpanel.dev/demo/shoey"
|
||||
rel="noreferrer noopener nofollow"
|
||||
@@ -118,39 +114,39 @@ export function Hero() {
|
||||
<Perks perks={perks} />
|
||||
</div>
|
||||
|
||||
<div className="col sm:w-1/2 relative group max-sm:px-4">
|
||||
<div className="col group relative max-sm:px-4 sm:w-1/2">
|
||||
<div
|
||||
className={cn([
|
||||
'overflow-hidden rounded-lg border border-border bg-background shadow-lg',
|
||||
'sm:absolute sm:left-0 sm:-top-12 sm:w-[800px] sm:-bottom-64',
|
||||
'max-sm:h-[800px] max-sm:-mx-4 max-sm:mt-12 relative',
|
||||
'sm:absolute sm:-top-12 sm:-bottom-64 sm:left-0 sm:w-[800px]',
|
||||
'relative max-sm:-mx-4 max-sm:mt-12 max-sm:h-[800px]',
|
||||
])}
|
||||
>
|
||||
{/* Window controls */}
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-muted/50 h-12">
|
||||
<div className="flex h-12 items-center gap-2 border-border border-b bg-muted/50 px-4 py-2">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-red-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-yellow-500" />
|
||||
<div className="h-3 w-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
{/* URL bar */}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className="group mx-4 flex flex-1 items-center gap-2 rounded-md border border-border bg-background/20 px-3 py-1 text-sm"
|
||||
href="https://demo.openpanel.dev/demo/shoey"
|
||||
className="group flex-1 mx-4 px-3 py-1 text-sm bg-background/20 rounded-md border border-border flex items-center gap-2"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="text-muted-foreground flex-1">
|
||||
<span className="flex-1 text-muted-foreground">
|
||||
https://demo.openpanel.dev
|
||||
</span>
|
||||
<ArrowRightIcon className="size-4 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<ArrowRightIcon className="size-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</a>
|
||||
</div>
|
||||
<iframe
|
||||
src={'https://demo.openpanel.dev/demo/shoey?range=lastHour'}
|
||||
className="w-full h-full"
|
||||
title="Live preview"
|
||||
className="h-full w-full"
|
||||
scrolling="no"
|
||||
src={'https://demo.openpanel.dev/demo/shoey?range=lastHour'}
|
||||
title="Live preview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,13 +171,13 @@ export function HeroContainer({
|
||||
<HeroImage />
|
||||
</div>
|
||||
<div
|
||||
className="container relative col sm:row py-44 max-sm:pt-32"
|
||||
className="col sm:row container relative py-44 max-sm:pt-32"
|
||||
data-children
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{divider && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-20 border-t border-border rounded-t-[3rem] md:rounded-t-[6rem] bg-background shadow-[0_0_100px_var(--background)]" />
|
||||
<div className="absolute right-0 bottom-0 left-0 h-20 rounded-t-[3rem] border-border border-t bg-background shadow-[0_0_100px_var(--background)] md:rounded-t-[6rem]" />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
type IllustrationProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function DataOwnershipIllustration({
|
||||
className = '',
|
||||
}: IllustrationProps) {
|
||||
return (
|
||||
<div>
|
||||
{/* Main layout */}
|
||||
<div className="relative grid aspect-2/1 grid-cols-5 gap-3">
|
||||
{/* Left: your server card */}
|
||||
<div
|
||||
className="
|
||||
col-span-3 rounded-2xl border border-border bg-card/80
|
||||
p-3 sm:p-4 shadow-xl backdrop-blur
|
||||
transition-all duration-300
|
||||
group-hover:-translate-y-1 group-hover:-translate-x-0.5
|
||||
"
|
||||
>
|
||||
<div className="flex items-center justify-between text-xs text-foreground">
|
||||
<span>Your server</span>
|
||||
<span className="flex items-center gap-1 rounded-full bg-card/80 px-2 py-0.5 text-[10px] text-blue-300">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-blue-400" />
|
||||
In control
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* "Server" visual */}
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="flex-1 rounded-xl bg-card/80 border border-border px-3 py-2">
|
||||
<p className="text-[10px] text-muted-foreground">Region</p>
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
EU / Custom
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 rounded-xl bg-card/80 border border-border px-3 py-2">
|
||||
<p className="text-[10px] text-muted-foreground">Retention</p>
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Configurable
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* mini "database"/requests strip */}
|
||||
<div className="mt-1 rounded-xl border border-border bg-card/90 px-3 py-2 text-[11px] text-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Events stored</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
locally
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
<div className="h-1.5 flex-1 rounded-full bg-blue-400/70" />
|
||||
<div className="h-1.5 flex-1 rounded-full bg-blue-400/40" />
|
||||
<div className="h-1.5 flex-1 rounded-full bg-blue-400/20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 rounded-xl border border-border bg-card/90 px-3 py-2 text-[11px] text-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>CPU</span>
|
||||
<span className="text-[10px] text-muted-foreground">20%</span>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
<div className="h-1.5 flex-1 rounded-full bg-blue-400/70" />
|
||||
<div className="h-1.5 flex-1 rounded-full bg-blue-400/40" />
|
||||
<div className="h-1.5 flex-1 rounded-full bg-blue-400/20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: third-party contrast */}
|
||||
<div
|
||||
className="
|
||||
col-span-2 rounded-2xl border border-border/80 bg-card/40
|
||||
p-3 text-[11px] text-muted-foreground
|
||||
transition-all duration-300
|
||||
group-hover:translate-y-1 group-hover:translate-x-0.5 group-hover:opacity-70
|
||||
"
|
||||
>
|
||||
<p className="text-xs text-muted-foreground mb-2">or use our cloud</p>
|
||||
|
||||
<ul className="space-y-1.5">
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="h-1 w-1 rounded-full bg-blue-400" />
|
||||
Zero server setup
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="h-1 w-1 rounded-full bg-blue-400" />
|
||||
Auto-scaling & backups
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="h-1 w-1 rounded-full bg-blue-400" />
|
||||
99.9% uptime
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="h-1 w-1 rounded-full bg-blue-400" />
|
||||
24/7 support
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="h-1 w-1 rounded-full bg-blue-400" />
|
||||
Export data anytime
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
type IllustrationProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function PrivacyIllustration({ className = '' }: IllustrationProps) {
|
||||
return (
|
||||
<div>
|
||||
{/* Floating cards */}
|
||||
<div className="relative aspect-3/2 md:aspect-2/1">
|
||||
{/* Back card */}
|
||||
<div
|
||||
className="
|
||||
absolute top-0 left-0 right-10 bottom-10 rounded-2xl border border-border/80 bg-card/70
|
||||
backdrop-blur-sm shadow-lg
|
||||
transition-all duration-300
|
||||
group-hover:-translate-y-1 group-hover:-rotate-2
|
||||
"
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 pt-3 text-xs text-muted-foreground">
|
||||
<span>Session duration</span>
|
||||
<span className="flex items-center gap-1">
|
||||
3m 12s
|
||||
<span className="text-[10px] text-blue-400">+8%</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Simple line chart */}
|
||||
<div className="mt-3 px-4">
|
||||
<svg
|
||||
viewBox="0 0 120 40"
|
||||
className="h-16 w-full text-muted-foreground"
|
||||
>
|
||||
<path
|
||||
d="M2 32 L22 18 L40 24 L60 10 L78 16 L96 8 L118 14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
className="opacity-60"
|
||||
/>
|
||||
<circle cx="118" cy="14" r="2.5" className="fill-blue-400" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Front card */}
|
||||
<div
|
||||
className="
|
||||
col
|
||||
absolute top-10 left-4 right-0 bottom-0 rounded-2xl border border-border/80
|
||||
bg-card shadow-xl
|
||||
transition-all duration-300
|
||||
group-hover:translate-y-1 group-hover:rotate-2
|
||||
"
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 pt-3 text-xs text-foreground">
|
||||
<span>Anonymous visitors</span>
|
||||
<span className="text-[10px] rounded-full bg-card px-2 py-0.5 text-muted-foreground">
|
||||
No cookies
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-between px-4 pt-4 pb-3">
|
||||
<div>
|
||||
<p className="text-[11px] text-muted-foreground mb-1">
|
||||
Active now
|
||||
</p>
|
||||
<p className="text-2xl font-semibold text-foreground">128</p>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-[10px] text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-blue-400" />
|
||||
<span>IP + UA hashed daily</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-blue-400" />
|
||||
<span>No fingerprinting</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* "Sources" row */}
|
||||
<div className="mt-auto flex gap-2 border-t border-border px-3 py-2.5 text-[11px]">
|
||||
<div className="flex-1 rounded-xl bg-card/90 px-3 py-1.5 flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Direct</span>
|
||||
<span className="text-foreground">42%</span>
|
||||
</div>
|
||||
<div className="flex-1 rounded-xl bg-card/90 px-3 py-1.5 flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Organic</span>
|
||||
<span className="text-foreground">58%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
'use client';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ResponsiveFunnel } from '@nivo/funnel';
|
||||
import NumberFlow from '@number-flow/react';
|
||||
import { AnimatePresence, motion, useSpring } from 'framer-motion';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function useFunnelSteps() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
return [
|
||||
{
|
||||
id: 'Visitors',
|
||||
label: 'Visitors',
|
||||
value: 10000,
|
||||
percentage: 100,
|
||||
color: resolvedTheme === 'dark' ? '#333' : '#888',
|
||||
},
|
||||
{
|
||||
id: 'Add to cart',
|
||||
label: 'Add to cart',
|
||||
value: 7000,
|
||||
percentage: 32,
|
||||
color: resolvedTheme === 'dark' ? '#222' : '#999',
|
||||
},
|
||||
{
|
||||
id: 'Checkout',
|
||||
label: 'Checkout',
|
||||
value: 5000,
|
||||
percentage: 8.9,
|
||||
color: resolvedTheme === 'dark' ? '#111' : '#e1e1e1',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function ProductAnalyticsIllustration() {
|
||||
return (
|
||||
<div className="aspect-video">
|
||||
<FunnelVisualization />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const PartLabel = ({ part }: { part: any }) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
return (
|
||||
<g transform={`translate(${part.x}, ${part.y})`}>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
style={{
|
||||
fill: resolvedTheme === 'dark' ? '#fff' : '#000',
|
||||
pointerEvents: 'none',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{part.data.label}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
function Labels(props: any) {
|
||||
return props.parts.map((part: any) => (
|
||||
<PartLabel key={part.data.id} part={part} />
|
||||
));
|
||||
}
|
||||
|
||||
function FunnelVisualization() {
|
||||
const funnelSteps = useFunnelSteps();
|
||||
const colors = funnelSteps.map((stage) => stage.color);
|
||||
const nivoData = funnelSteps.map((stage) => ({
|
||||
id: stage.id,
|
||||
value: stage.value,
|
||||
label: stage.label,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<ResponsiveFunnel
|
||||
data={nivoData}
|
||||
margin={{ top: 20, right: 0, bottom: 20, left: 0 }}
|
||||
direction="horizontal"
|
||||
shapeBlending={0.6}
|
||||
colors={colors}
|
||||
enableBeforeSeparators={false}
|
||||
enableAfterSeparators={false}
|
||||
beforeSeparatorLength={0}
|
||||
afterSeparatorLength={0}
|
||||
afterSeparatorOffset={0}
|
||||
beforeSeparatorOffset={0}
|
||||
currentPartSizeExtension={5}
|
||||
borderWidth={20}
|
||||
currentBorderWidth={15}
|
||||
tooltip={() => null}
|
||||
layers={['parts', Labels]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
'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 = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Ftwitter.com',
|
||||
name: 'Twitter',
|
||||
percentage: 10,
|
||||
value: 412,
|
||||
},
|
||||
];
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
export function WebAnalyticsIllustration() {
|
||||
const [currentSourceIndex, setCurrentSourceIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSourceIndex((prev) => (prev + 1) % TRAFFIC_SOURCES.length);
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="px-12 group aspect-video">
|
||||
<div className="relative h-full col">
|
||||
<MetricCard
|
||||
title="Session duration"
|
||||
value="3m 23s"
|
||||
change="3%"
|
||||
chartPoints={[40, 10, 20, 43, 20, 40, 30, 37, 40, 34, 50, 31]}
|
||||
color="var(--foreground)"
|
||||
className="absolute w-full rotate-0 top-2 left-2 group-hover:-translate-y-1 group-hover:-rotate-2 transition-all duration-300"
|
||||
/>
|
||||
<MetricCard
|
||||
title="Bounce rate"
|
||||
value="46%"
|
||||
change="3%"
|
||||
chartPoints={[10, 46, 20, 43, 20, 40, 30, 37, 40, 34, 50, 31]}
|
||||
color="var(--foreground)"
|
||||
className="absolute w-full -rotate-2 -left-2 top-12 group-hover:-translate-y-1 group-hover:rotate-0 transition-all duration-300"
|
||||
/>
|
||||
<div className="col gap-4 w-[80%] md:w-[70%] ml-auto mt-auto">
|
||||
<BarCell
|
||||
{...TRAFFIC_SOURCES[currentSourceIndex]}
|
||||
className="group-hover:scale-105 transition-all duration-300"
|
||||
/>
|
||||
<BarCell
|
||||
{...TRAFFIC_SOURCES[
|
||||
(currentSourceIndex + 1) % TRAFFIC_SOURCES.length
|
||||
]}
|
||||
className="group-hover:scale-105 transition-all duration-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
title,
|
||||
value,
|
||||
change,
|
||||
chartPoints,
|
||||
color,
|
||||
className,
|
||||
}: {
|
||||
title: string;
|
||||
value: string;
|
||||
change: string;
|
||||
chartPoints: number[];
|
||||
color: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('col bg-card rounded-lg p-4 pb-6 border', className)}>
|
||||
<div className="row items-end justify-between">
|
||||
<div>
|
||||
<div className="text-muted-foreground text-sm">{title}</div>
|
||||
<div className="text-2xl font-semibold font-mono">{value}</div>
|
||||
</div>
|
||||
<div className="row gap-2 items-center font-mono font-medium">
|
||||
<ArrowUpIcon className="size-3" strokeWidth={3} />
|
||||
<div>{change}</div>
|
||||
</div>
|
||||
</div>
|
||||
<SimpleChart
|
||||
width={400}
|
||||
height={30}
|
||||
points={chartPoints}
|
||||
strokeColor={color}
|
||||
className="mt-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BarCell({
|
||||
icon,
|
||||
name,
|
||||
percentage,
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
icon: string;
|
||||
name: string;
|
||||
percentage: number;
|
||||
value: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative p-4 py-2 bg-card rounded-lg shadow-[0_10px_30px_rgba(0,0,0,0.3)] border',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="absolute bg-background bottom-0 top-0 left-0 rounded-lg transition-all duration-500"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="relative row justify-between ">
|
||||
<div className="row gap-2 items-center font-medium text-sm">
|
||||
{icon.startsWith('http') ? (
|
||||
<Image
|
||||
alt="serie icon"
|
||||
className="max-h-4 rounded-[2px] object-contain"
|
||||
src={icon}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-2xl">{icon}</div>
|
||||
)}
|
||||
<AnimatePresence mode="popLayout">
|
||||
<motion.div
|
||||
key={name}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{name}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="row gap-3 font-mono text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
<NumberFlow value={percentage} />%
|
||||
</span>
|
||||
<NumberFlow value={value} locales={'en-US'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import NumberFlow from '@number-flow/react';
|
||||
import { PRICING } from '@openpanel/payments/prices';
|
||||
import { CheckIcon, ServerIcon, StarIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import NumberFlow from '@number-flow/react';
|
||||
import { PRICING } from '@openpanel/payments/prices';
|
||||
|
||||
import { CheckIcon, StarIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { formatEventsCount } from '@/lib/utils';
|
||||
import { cn, formatEventsCount } from '@/lib/utils';
|
||||
|
||||
const features = [
|
||||
'Unlimited websites or apps',
|
||||
@@ -28,51 +26,56 @@ export function Pricing() {
|
||||
return (
|
||||
<Section className="container">
|
||||
<div className="col md:row gap-16">
|
||||
<div className="w-full md:w-1/3 min-w-sm col gap-4 border rounded-3xl p-6 bg-linear-to-b from-card to-background">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="col w-full min-w-sm gap-4 rounded-3xl border bg-linear-to-b from-card to-background p-6 md:w-1/3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Choose how many events you'll track this month
|
||||
</p>
|
||||
<div className="row flex-wrap gap-2">
|
||||
{PRICING.map((tier, index) => (
|
||||
<Button
|
||||
className={cn('relative h-8 rounded-full border px-4')}
|
||||
key={tier.price}
|
||||
variant={selectedIndex === index ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn('h-8 rounded-full relative px-4 border')}
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
size="sm"
|
||||
variant={selectedIndex === index ? 'default' : 'outline'}
|
||||
>
|
||||
{tier.popular && <StarIcon className="size-4" />}
|
||||
{formatEventsCount(tier.events)}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant={selectedIndex === -1 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn('h-8 rounded-full relative px-4 border')}
|
||||
className={cn('relative h-8 rounded-full border px-4')}
|
||||
onClick={() => setSelectedIndex(-1)}
|
||||
size="sm"
|
||||
variant={selectedIndex === -1 ? 'default' : 'outline'}
|
||||
>
|
||||
Custom
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col items-baseline mt-8 md:mt-auto w-full">
|
||||
<div className="col mt-8 w-full items-baseline md:mt-auto">
|
||||
{selected ? (
|
||||
<>
|
||||
<NumberFlow
|
||||
className="text-5xl font-bold"
|
||||
value={selected.price}
|
||||
format={{
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
}}
|
||||
locales={'en-US'}
|
||||
/>
|
||||
<div className="row justify-between w-full">
|
||||
<span className="text-muted-foreground/80 text-sm -mt-2">
|
||||
<div className="row items-end gap-3">
|
||||
<NumberFlow
|
||||
className="font-bold text-5xl"
|
||||
format={{
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
}}
|
||||
locales={'en-US'}
|
||||
value={selected.price}
|
||||
/>
|
||||
<span className="mb-2 rounded-full bg-primary/10 px-2.5 py-0.5 font-medium text-primary text-xs">
|
||||
30-day free trial
|
||||
</span>
|
||||
</div>
|
||||
<div className="row w-full justify-between">
|
||||
<span className="-mt-2 text-muted-foreground/80 text-sm">
|
||||
Per month
|
||||
</span>
|
||||
<span className="text-muted-foreground/80 text-sm -mt-2">
|
||||
<span className="-mt-2 text-muted-foreground/80 text-sm">
|
||||
+ VAT if applicable
|
||||
</span>
|
||||
</div>
|
||||
@@ -89,18 +92,18 @@ export function Pricing() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col gap-8 justify-center flex-1 shrink-0">
|
||||
<div className="col flex-1 shrink-0 justify-center gap-8">
|
||||
<div className="col gap-4">
|
||||
<SectionHeader
|
||||
title="Simple, transparent pricing"
|
||||
description="Pay only for what you use. Choose your event volume - everything else is unlimited. No surprises, no hidden fees."
|
||||
title="Simple, transparent pricing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul className="col gap-2">
|
||||
{features.map((feature) => (
|
||||
<li key={feature} className="row gap-2 items-start text-sm">
|
||||
<CheckIcon className="size-4 shrink-0 mt-0.5" />
|
||||
<li className="row items-start gap-2 text-sm" key={feature}>
|
||||
<CheckIcon className="mt-0.5 size-4 shrink-0" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -108,9 +111,16 @@ export function Pricing() {
|
||||
|
||||
<GetStartedButton className="w-fit" />
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
All features are included upfront - no hidden costs. You choose how
|
||||
many events to track each month.
|
||||
<p className="row items-center gap-2 text-muted-foreground text-sm">
|
||||
<ServerIcon className="size-4 shrink-0" />
|
||||
Prefer to self-host?{' '}
|
||||
<Link
|
||||
className="text-primary hover:underline"
|
||||
href="/docs/self-hosting/self-hosting"
|
||||
>
|
||||
Deploy for free
|
||||
</Link>{' '}
|
||||
with unlimited events.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user