improve(public): re-design landing page a bit

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-06-18 14:38:14 +02:00
parent 9b16bbaccd
commit 5c6d71f176
23 changed files with 859 additions and 154 deletions

View File

@@ -0,0 +1,28 @@
'use client';
import {
BatteryFullIcon,
BatteryLowIcon,
BatteryMediumIcon,
} from 'lucide-react';
import { useEffect, useState } from 'react';
export function BatteryIcon({ className }: { className?: string }) {
const [index, setIndex] = useState(0);
const icons = [BatteryLowIcon, BatteryMediumIcon, BatteryFullIcon];
const Icon = icons[index];
useEffect(() => {
const interval = setInterval(() => {
setIndex((index + 1) % icons.length);
}, 750);
return () => clearInterval(interval);
}, [index]);
if (!Icon) {
return <div className={className} />;
}
return <Icon className={className} />;
}

View File

@@ -0,0 +1,81 @@
'use client';
import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useState } from 'react';
const brandConfig = {
Mixpanel: '#7A59FF',
'Google Analytics': '#E37400',
Amplitude: '#00CF98',
} as const;
const words = Object.keys(brandConfig);
function useWordCycle(words: string[], interval: number, mounted: boolean) {
const [index, setIndex] = useState(0);
const [isInitial, setIsInitial] = useState(true);
useEffect(() => {
if (!mounted) {
return;
}
if (isInitial) {
setIndex(Math.floor(Math.random() * words.length));
setIsInitial(false);
return;
}
const timer = setInterval(() => {
setIndex((current) => (current + 1) % words.length);
}, interval);
return () => clearInterval(timer);
}, [words, interval, isInitial, mounted]);
return words[index];
}
export function Competition() {
const [mounted, setMounted] = useState(false);
const word = useWordCycle(words, 2100, mounted);
const color = brandConfig[word as keyof typeof brandConfig];
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<span className="block truncate leading-tight -mt-1" style={{ color }}>
{word}
</span>
);
}
return (
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={word}
className="block truncate leading-tight -mt-1"
style={{ color }}
>
{word?.split('').map((char, index) => (
<motion.span
key={`${word}-${char}-${index.toString()}`}
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -10, opacity: 0 }}
transition={{
duration: 0.15,
delay: index * 0.015,
ease: 'easeOut',
}}
style={{ display: 'inline-block', whiteSpace: 'pre' }}
>
{char}
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
}

View File

@@ -1,7 +1,28 @@
import { cn } from '@/lib/utils';
import { ChevronRightIcon } from 'lucide-react';
import { ChevronRightIcon, ConeIcon } from 'lucide-react';
import Link from 'next/link';
export function SmallFeature({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
className={cn(
'bg-background-light rounded-lg p-1 border border-border group',
className,
)}
>
<div className="bg-background-dark rounded-lg p-8 h-full group-hover:bg-background-light transition-colors">
{children}
</div>
</div>
);
}
export function Feature({
children,
media,
@@ -16,7 +37,7 @@ export function Feature({
return (
<div
className={cn(
'border rounded-lg bg-background-light overflow-hidden',
'border rounded-lg bg-background-light overflow-hidden p-1',
className,
)}
>
@@ -30,7 +51,7 @@ export function Feature({
{media && (
<div
className={cn(
'bg-background-dark h-full',
'bg-background-dark h-full rounded-md overflow-hidden',
reverse && 'md:order-first',
)}
>
@@ -50,13 +71,16 @@ export function FeatureContent({
}: {
icon?: React.ReactNode;
title: string;
content: string[];
content: React.ReactNode[];
className?: string;
}) {
return (
<div className={className}>
{icon && (
<div className="bg-foreground text-background rounded-md p-4 inline-block mb-1">
<div
data-icon
className="bg-foreground text-background rounded-md p-4 inline-block mb-6 transition-colors"
>
{icon}
</div>
)}
@@ -72,6 +96,17 @@ export function FeatureContent({
);
}
export function FeatureListItem({
icon: Icon,
title,
}: { icon: React.ComponentType<any>; title: string }) {
return (
<div className="row items-center gap-2" key="funnel">
<Icon className="size-4 text-foreground/70" strokeWidth={1.5} /> {title}
</div>
);
}
export function FeatureList({
title,
items,

View File

@@ -59,23 +59,16 @@ export function Footer() {
</ul>
</div>
{/* <div className="col gap-3">
<h3 className="font-medium">Company</h3>
<ul className="gap-2 col text-muted-foreground">
<li>
<Link href="/about">About</Link>
</li>
<li>
<Link href="/contact">Contact</Link>
</li>
</ul>
</div> */}
<div className="col gap-3 ">
<h3 className="font-medium">Comparisons</h3>
<h3 className="font-medium">Articles</h3>
<ul className="gap-2 col text-muted-foreground">
<li>
<Link href="/articles/vs-mixpanel">vs Mixpanel</Link>
<Link href="/articles/vs-mixpanel">OpenPanel vs Mixpanel</Link>
</li>
<li>
<Link href="/articles/mixpanel-alternatives">
Mixpanel alternatives
</Link>
</li>
</ul>
</div>

View File

@@ -1,46 +1,125 @@
import { cn } from '@/lib/utils';
import { DollarSignIcon } from 'lucide-react';
import {
ArrowRightIcon,
CalendarIcon,
CheckIcon,
ChevronRightIcon,
CookieIcon,
CreditCardIcon,
DatabaseIcon,
DollarSignIcon,
FlaskRoundIcon,
GithubIcon,
ServerIcon,
StarIcon,
} from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
import { Competition } from './competition';
import { HeroCarousel } from './hero-carousel';
import { HeroMap } from './hero-map';
import { Tag } from './tag';
import { Button } from './ui/button';
const perks = [
{ text: 'Free trial 30 days', icon: CalendarIcon },
{ text: 'No credit card required', icon: CreditCardIcon },
{ text: 'Cookie-less tracking', icon: CookieIcon },
{ text: 'Open-source', icon: GithubIcon },
{ text: 'Your data, your rules', icon: DatabaseIcon },
{ text: 'Self-hostable', icon: ServerIcon },
];
export function Hero() {
return (
<HeroContainer>
{/* Shadow bottom */}
<div className="w-full absolute bottom-0 h-32 bg-gradient-to-t from-background to-transparent z-20" />
{/* Content */}
<div className="container relative z-10">
<div className="max-w-2xl col gap-4 pt-44 text-center mx-auto ">
<Tag className="self-center">
<DollarSignIcon className="size-4" />
Release 1.0 is live!
</Tag>
<h1 className="text-4xl md:text-6xl font-bold leading-[1.1]">
An open-source alternative to <span>Mixpanel</span>
</h1>
<p className="text-xl text-muted-foreground">
The power of Mixpanel, the ease of Plausible and nothing from Google
Analytics 😉
</p>
</div>
{/* CTA */}
<div className="col md:row gap-4 center-center my-12">
<Button size="lg" asChild>
<div className="container relative z-10 col sm:row sm:py-44 max-sm:pt-32">
<div className="col gap-8 w-full sm:w-1/2 sm:pr-12">
<div className="col gap-4">
<Tag className="self-start">
<StarIcon className="size-4 fill-yellow-500 text-yellow-500" />
Trusted by +2000 projects
</Tag>
<h1
className="text-4xl md:text-5xl font-extrabold leading-[1.1]"
title="An open-source alternative to Mixpanel"
>
An open-source alternative to <Competition />
</h1>
<p className="text-xl text-muted-foreground">
A web and product analytics platform that combines the power of
Mixpanel with the ease of Plausible and one of the best Google
Analytics replacements.
</p>
</div>
<Button size="lg" asChild className="group w-72">
<Link href="https://dashboard.openpanel.dev/onboarding">
Try it for free
Get started now
<ChevronRightIcon className="size-4 group-hover:translate-x-1 transition-transform group-hover:scale-125" />
</Link>
</Button>
<p className="text-sm text-muted-foreground">
Free trial for 30 days, no credit card required
</p>
<ul className="grid grid-cols-2 gap-2">
{perks.map((perk) => (
<li key={perk.text} className="text-sm text-muted-foreground">
<perk.icon className="size-4 inline-block mr-1" />
{perk.text}
</li>
))}
</ul>
</div>
<HeroCarousel />
<div className="col sm:w-1/2 relative group">
<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-32',
'max-sm:h-[800px] max-sm:-mx-4 max-sm:mt-12 relative',
])}
>
{/* 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 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>
{/* URL bar */}
<a
target="_blank"
rel="noreferrer noopener nofollow"
href="https://demo.openpanel.dev/demo/shoey"
className="group flex-1 mx-4 px-3 py-1 text-sm bg-background rounded-md border border-border flex items-center gap-2"
>
<span className="text-muted-foreground flex-1">
https://demo.openpanel.dev
</span>
<ArrowRightIcon className="size-4 opacity-0 group-hover:opacity-100 transition-opacity" />
</a>
</div>
<iframe
src={'https://demo.openpanel.dev/demo/shoey?range=lastHour'}
className="w-full h-full"
title="Live preview"
scrolling="no"
/>
<div className="pointer-events-none absolute inset-0 top-12 center-center group-hover:bg-foreground/20 transition-colors">
<Button
asChild
className="group-hover:opacity-100 opacity-0 transition-opacity pointer-events-auto"
>
<Link
href="https://demo.openpanel.dev/demo/shoey"
rel="noreferrer noopener nofollow"
target="_blank"
>
Test live demo
<FlaskRoundIcon className="size-4" />
</Link>
</Button>
</div>
</div>
</div>
</div>
</HeroContainer>
);
@@ -55,13 +134,6 @@ export function HeroContainer({
}): React.ReactElement {
return (
<section className={cn('radial-gradient overflow-hidden relative')}>
{/* Map */}
<HeroMap />
{/* Gradient over map */}
<div className="absolute inset-0 radial-gradient-dot-1 select-none" />
<div className="absolute inset-0 radial-gradient-dot-1 select-none" />
<div className={cn('relative z-10', className)}>{children}</div>
{/* Shadow bottom */}

View File

@@ -2,25 +2,37 @@ import {
Feature,
FeatureContent,
FeatureList,
FeatureListItem,
FeatureMore,
SmallFeature,
} from '@/components/feature';
import { Section, SectionHeader } from '@/components/section';
import { Tag } from '@/components/tag';
import {
ActivityIcon,
AreaChartIcon,
BarChart2Icon,
BarChartIcon,
BatteryIcon,
CheckIcon,
ClockIcon,
CloudIcon,
ConeIcon,
CookieIcon,
DatabaseIcon,
GithubIcon,
LayersIcon,
LineChartIcon,
LockIcon,
MapIcon,
PieChartIcon,
ServerIcon,
Share2Icon,
ShieldIcon,
UserIcon,
WalletIcon,
ZapIcon,
} from 'lucide-react';
import { BatteryIcon } from '../battery-icon';
import { EventsFeature } from './features/events-feature';
import { ProductAnalyticsFeature } from './features/product-analytics-feature';
import { ProfilesFeature } from './features/profiles-feature';
@@ -52,21 +64,30 @@ export function Features() {
className="mt-4"
title="Get a quick overview"
items={[
'• Visitors',
'• Referrals',
'• Top pages',
'• Top entries',
'• Top exists',
'• Devices',
'• Sessions',
'• Bounce rate',
'• Duration',
'• Geography',
<FeatureListItem key="line" icon={CheckIcon} title="Visitors" />,
<FeatureListItem key="line" icon={CheckIcon} title="Referrals" />,
<FeatureListItem key="line" icon={CheckIcon} title="Top pages" />,
<FeatureListItem
key="line"
icon={CheckIcon}
title="Top entries"
/>,
<FeatureListItem
key="line"
icon={CheckIcon}
title="Top exists"
/>,
<FeatureListItem key="line" icon={CheckIcon} title="Devices" />,
<FeatureListItem key="line" icon={CheckIcon} title="Sessions" />,
<FeatureListItem
key="line"
icon={CheckIcon}
title="Bounce rate"
/>,
<FeatureListItem key="line" icon={CheckIcon} title="Duration" />,
<FeatureListItem key="line" icon={CheckIcon} title="Geography" />,
]}
/>
{/* <FeatureMore href="#" className="mt-4">
And mouch more
</FeatureMore> */}
</Feature>
<Feature reverse media={<ProductAnalyticsFeature />}>
@@ -76,72 +97,49 @@ export function Features() {
'Turn data into decisions with powerful visualizations and real-time insights.',
]}
/>
<FeatureList
className="mt-4"
title="Understand your product"
items={[
<FeatureListItem key="funnel" icon={ConeIcon} title="Funnel" />,
<FeatureListItem
key="retention"
icon={UserIcon}
title="Retention"
/>,
<FeatureListItem
key="bar"
icon={BarChartIcon}
title="A/B tests"
/>,
<FeatureListItem
key="pie"
icon={PieChartIcon}
title="Conversion"
/>,
]}
/>
<FeatureList
className="mt-4"
title="Supported charts"
items={[
<div className="row items-center gap-2" key="line">
<LineChartIcon
className="size-4 text-foreground/70"
strokeWidth={1.5}
/>{' '}
Line
</div>,
<div className="row items-center gap-2" key="bar">
<BarChartIcon
className="size-4 text-foreground/70"
strokeWidth={1.5}
/>{' '}
Bar
</div>,
<div className="row items-center gap-2" key="pie">
<PieChartIcon
className="size-4 text-foreground/70"
strokeWidth={1.5}
/>{' '}
Pie
</div>,
<div className="row items-center gap-2" key="area">
<AreaChartIcon
className="size-4 text-foreground/70"
strokeWidth={1.5}
/>{' '}
Area
</div>,
<div className="row items-center gap-2" key="histogram">
<BarChart2Icon
className="size-4 text-foreground/70"
strokeWidth={1.5}
/>{' '}
Histogram
</div>,
<div className="row items-center gap-2" key="map">
<MapIcon
className="size-4 text-foreground/70"
strokeWidth={1.5}
/>{' '}
Map
</div>,
<div className="row items-center gap-2" key="funnel">
<ConeIcon
className="size-4 text-foreground/70"
strokeWidth={1.5}
/>{' '}
Funnel
</div>,
<div className="row items-center gap-2" key="retention">
<UserIcon
className="size-4 text-foreground/70"
strokeWidth={1.5}
/>{' '}
Retention
</div>,
<FeatureListItem key="line" icon={LineChartIcon} title="Line" />,
<FeatureListItem key="bar" icon={BarChartIcon} title="Bar" />,
<FeatureListItem key="pie" icon={PieChartIcon} title="Pie" />,
<FeatureListItem key="area" icon={AreaChartIcon} title="Area" />,
<FeatureListItem
key="histogram"
icon={BarChart2Icon}
title="Histogram"
/>,
<FeatureListItem key="map" icon={MapIcon} title="Map" />,
]}
/>
</Feature>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Feature>
<SmallFeature className="[&_[data-icon]]:hover:bg-blue-500">
<FeatureContent
icon={<ClockIcon className="size-8" strokeWidth={1} />}
title="Real time analytics"
@@ -149,8 +147,8 @@ export function Features() {
'Get instant insights into your data. No need to wait for data to be processed, like other tools out there, looking at you GA4...',
]}
/>
</Feature>
<Feature>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-purple-500">
<FeatureContent
icon={<DatabaseIcon className="size-8" strokeWidth={1} />}
title="Own your own data"
@@ -159,10 +157,8 @@ export function Features() {
'Self-host it on your own infrastructure to have complete control.',
]}
/>
</Feature>
<div />
<div />
<Feature>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-indigo-500">
<FeatureContent
icon={<CloudIcon className="size-8" strokeWidth={1} />}
title="Cloud or self-hosted"
@@ -176,8 +172,8 @@ export function Features() {
>
More about self-hosting
</FeatureMore>
</Feature>
<Feature>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-green-500">
<FeatureContent
icon={<CookieIcon className="size-8" strokeWidth={1} />}
title="No cookies"
@@ -186,7 +182,93 @@ export function Features() {
'We follow GDPR guidelines closely, ensuring your personal information is protected without using invasive technologies.',
]}
/>
</Feature>
<FeatureMore
href="/articles/cookieless-analytics"
className="mt-4 -mb-4"
>
Cookieless analytics
</FeatureMore>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-gray-500">
<FeatureContent
icon={<GithubIcon className="size-8" strokeWidth={1} />}
title="Open-source"
content={[
'Our code is open and transparent. Contribute, fork, or learn from our implementation.',
]}
/>
<FeatureMore
href="https://git.new/openpanel"
className="mt-4 -mb-4"
>
View the code
</FeatureMore>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-purple-500">
<FeatureContent
icon={<LockIcon className="size-8" strokeWidth={1} />}
title="Your data, your rules"
content={[
'Complete control over your data. Export, delete, or manage it however you need.',
]}
/>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-yellow-500">
<FeatureContent
icon={<WalletIcon className="size-8" strokeWidth={1} />}
title="Affordably priced"
content={[
'Transparent pricing that scales with your needs. No hidden fees or surprise charges.',
]}
/>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-orange-500">
<FeatureContent
icon={<ZapIcon className="size-8" strokeWidth={1} />}
title="Moving fast"
content={[
'Regular updates and improvements. We move quickly to add features you need.',
]}
/>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-blue-500">
<FeatureContent
icon={<ActivityIcon className="size-8" strokeWidth={1} />}
title="Real-time data"
content={[
'See your analytics as they happen. No waiting for data processing or updates.',
]}
/>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-indigo-500">
<FeatureContent
icon={<Share2Icon className="size-8" strokeWidth={1} />}
title="Sharable reports"
content={[
'Easily share insights with your team. Export and distribute reports with a single click.',
<i key="soon">Coming soon</i>,
]}
/>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-pink-500">
<FeatureContent
icon={<BarChart2Icon className="size-8" strokeWidth={1} />}
title="Visualize your data"
content={[
'Beautiful, interactive visualizations that make your data easy to understand.',
]}
/>
</SmallFeature>
<SmallFeature className="[&_[data-icon]]:hover:bg-indigo-500">
<FeatureContent
icon={<LayersIcon className="size-8" strokeWidth={1} />}
title="Best of both worlds"
content={[
'Combine the power of self-hosting with the convenience of cloud deployment.',
]}
/>
</SmallFeature>
</div>
<Feature media={<EventsFeature />}>

View File

@@ -1,4 +1,5 @@
import { CheckIcon, DollarSignIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { CheckIcon, ChevronRightIcon, DollarSignIcon } from 'lucide-react';
import Link from 'next/link';
import { DoubleSwirl } from '../Swirls';
import { PricingSlider } from '../pricing-slider';
@@ -7,9 +8,14 @@ import { Tag } from '../tag';
import { Button } from '../ui/button';
export default Pricing;
export function Pricing() {
export function Pricing({ className }: { className?: string }) {
return (
<Section className="overflow-hidden relative bg-foreground dark:bg-background-dark text-background dark:text-foreground rounded-xl p-0 pb-32 pt-16 max-w-7xl mx-auto">
<Section
className={cn(
'overflow-hidden relative bg-foreground dark:bg-background-dark text-background dark:text-foreground xl:rounded-xl p-0 pb-32 pt-16 max-w-7xl mx-auto',
className,
)}
>
<DoubleSwirl className="absolute top-0 left-0" />
<div className="container relative z-10">
<SectionHeader
@@ -20,7 +26,7 @@ export function Pricing() {
</Tag>
}
title="Simple pricing"
description="Our simple, usage-based pricing means you only pay for what you use. Scale effortlessly for the best value."
description="Just pick how many events you want to track each month. No hidden costs."
/>
<div className="grid md:grid-cols-[400px_1fr] gap-8">
@@ -52,9 +58,15 @@ export function Pricing() {
</li>
</ul>
<Button variant="secondary" className="self-start mt-4" asChild>
<Button
variant="secondary"
size="lg"
asChild
className="self-start mt-4 px-8 group"
>
<Link href="https://dashboard.openpanel.dev/onboarding">
Start for free
Get started now
<ChevronRightIcon className="size-4 group-hover:translate-x-1 transition-transform group-hover:scale-125" />
</Link>
</Button>
</div>

View File

@@ -42,7 +42,7 @@ export function TwitterCard({
};
return (
<div className="border rounded-lg p-4 col gap-4 bg-background-light">
<div className="border rounded-lg p-8 col gap-4 bg-background-light">
<div className="row gap-4">
<div className="size-12 rounded-full bg-muted overflow-hidden shrink-0">
{avatarUrl && (

View File

@@ -0,0 +1,87 @@
import { cn } from '@/lib/utils';
import { ArrowDownIcon } from 'lucide-react';
import Image from 'next/image';
import { Section, SectionHeader } from './section';
import { Tag } from './tag';
import { Tooltip } from './ui/tooltip';
const images = [
{
name: 'Helpy UI',
url: 'https://helpy-ui.com',
logo: 'helpy-ui.png',
border: true,
},
{
name: 'KiddoKitchen',
url: 'https://kiddokitchen.se',
logo: 'kiddokitchen.png',
border: false,
},
{
name: 'Maneken',
url: 'https://maneken.app',
logo: 'maneken.jpg',
border: false,
},
{
name: 'Midday',
url: 'https://midday.ai',
logo: 'midday.png',
border: true,
},
{
name: 'Screenzen',
url: 'https://www.screenzen.co',
logo: 'screenzen.avif',
border: true,
},
{
name: 'Tiptip',
url: 'https://tiptip.id',
logo: 'tiptip.jpg',
border: true,
},
];
export function WhyOpenPanel() {
return (
<div className="bg-background-light my-12 col">
<Section className="container my-0 py-20">
<SectionHeader
title="Why OpenPanel?"
description="We built OpenPanel to get the best of both web and product analytics. With that in mind we have created a simple but very powerful platform that can handle most companies needs."
/>
<div className="center-center col gap-4 -mt-4">
<Tag>
<ArrowDownIcon className="size-4" strokeWidth={1.5} />
With 2000+ registered projects
</Tag>
<div className="row gap-4 justify-center flex-wrap">
{images.map((image) => (
<a
href={image.url}
target="_blank"
rel="noopener noreferrer nofollow"
key={image.logo}
className={cn(
'group rounded-lg bg-white center-center size-20 hover:scale-110 transition-all duration-300',
image.border && 'p-2 border border-border shadow-sm',
)}
title={image.name}
>
<Image
src={`/logos/${image.logo}`}
alt={image.name}
width={80}
height={80}
className="rounded-lg grayscale group-hover:grayscale-0 transition-all duration-300"
/>
</a>
))}
</div>
</div>
</Section>
</div>
);
}