re-design public site

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-03-05 22:07:10 +01:00
parent 7cd8ec846b
commit 6c109dde0e
20 changed files with 265 additions and 137 deletions

View File

@@ -13,25 +13,23 @@ import Image from 'next/image';
const images = [
{
title: 'Beautiful overview, everything is clickable to get more details',
url: '/demo/overview-min.png',
url: '/demo-2/1.png',
},
{
title: 'Histogram, perfect for showing active users',
url: '/demo/histogram-min.png',
url: '/demo-2/2.png',
},
{ title: 'Make your overview public', url: '/demo/overview-share-min.png' },
{ title: 'Make your overview public', url: '/demo-2/3.png' },
{
title: 'See real time events from your users',
url: '/demo/events-min.png',
url: '/demo-2/4.png',
},
{ title: 'The classic line chart', url: '/demo/line-min.png' },
{ title: 'The classic line chart', url: '/demo-2/5.png' },
{
title: 'Bar charts to see your most popular content',
url: '/demo/bar-min.png',
url: '/demo-2/6.png',
},
{ title: 'Get nice metric cards with graphs', url: '/demo/metrics-min.png' },
{ title: 'See where your events comes from', url: '/demo/worldmap-min.png' },
{ title: 'The classic pie chart', url: '/demo/pie-min.png' },
{ title: 'Get nice metric cards with graphs', url: '/demo-2/7.png' },
];
export function PreviewCarousel() {
@@ -51,13 +49,18 @@ export function PreviewCarousel() {
key={item.url}
className="flex-[0_0_80%] max-w-3xl pl-8"
>
<div className="aspect-video">
<div className="p-3 rounded-xl overflow-hidden bg-gradient-to-b from-blue-100/50 to-white/50">
<div
style={{
aspectRatio: 2982 / 1484,
}}
>
<div className="p-1 rounded-xl overflow-hidden bg-gradient-to-b from-blue-100/50 to-white/50">
<Image
priority
className="w-full h-full object-cover rounded-lg"
src={item.url}
width={1080}
height={608}
width={2982 * 0.5}
height={1484 * 0.5}
alt={item.title}
/>
</div>

View File

@@ -26,7 +26,10 @@ export function Paragraph({ children, className }: Props) {
export function Heading1({ children, className }: Props) {
return (
<h1
className={cn('text-4xl md:text-5xl font-bold text-slate-800', className)}
className={cn(
'text-4xl md:text-5xl font-bold text-slate-800 !leading-tight',
className
)}
>
{children}
</h1>

View File

@@ -20,9 +20,9 @@ export default function Footer() {
</div>
<div className="overflow-hidden rounded-xl">
<div className="p-3 bg-white/20">
<div className="p-2 bg-white/20">
<Image
src="/demo/overview-min.png"
src="/demo-2/1.png"
width={1080}
height={608}
alt="Openpanel overview page"
@@ -31,13 +31,13 @@ export default function Footer() {
</div>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0">
<div className="-mt-8 relative z-10">
<div className="h-px w-full bg-[radial-gradient(circle,rgba(255,255,255,0.7)_0%,rgba(255,255,255,0.7)_50%,rgba(255,255,255,0)_100%)]"></div>
<div className="p-4 bg-blue-darker">
<div className="container">
<div className="flex justify-between items-center text-sm">
<div className="flex flex-col gap-4 md:flex-row md:justify-between md:items-center text-sm">
<Logo />
<div className="flex gap-4">
<div className="flex flex-col md:flex-row gap-4">
<Link className="hover:underline" href="/terms">
Terms and Conditions
</Link>

View File

@@ -1,76 +1,56 @@
// background-image: radial-gradient(circle at 1px 1px, black 1px, transparent 0);
// background-size: 40px 40px;
import { Logo } from '@/components/Logo';
import {
BarChart2Icon,
CookieIcon,
Globe2Icon,
LayoutPanelTopIcon,
LockIcon,
ServerIcon,
} from 'lucide-react';
import Image from 'next/image';
import { Heading1, Lead, Lead2 } from './copy';
import { JoinWaitlist } from './join-waitlist';
import { PreviewCarousel } from './carousel';
import { Heading1, Lead2 } from './copy';
import { JoinWaitlistHero } from './join-waitlist-hero';
const features = [
{
title: 'Great overview',
icon: LayoutPanelTopIcon,
},
{
title: 'Beautiful charts',
icon: BarChart2Icon,
},
{
title: 'Privacy focused',
icon: LockIcon,
},
{
title: 'Open-source',
icon: Globe2Icon,
},
{
title: 'No cookies',
icon: CookieIcon,
},
{
title: 'Self-hosted',
icon: ServerIcon,
},
const avatars = [
'https://api.dicebear.com/7.x/adventurer/svg?seed=Chester&backgroundColor=b6e3f4',
'https://api.dicebear.com/7.x/adventurer/svg?seed=Casper&backgroundColor=c0aede',
'https://api.dicebear.com/7.x/adventurer/svg?seed=Boo&backgroundColor=ffdfbf',
];
export function Hero({ waitlistCount }: { waitlistCount: number }) {
return (
<div className="flex flex-col items-center w-full text-center text-blue-950">
<div className="pt-32 pb-56 p-4 flex flex-col items-center max-w-3xl bg-[radial-gradient(circle,rgba(255,255,255,0.7)_0%,rgba(255,255,255,0.7)_50%,rgba(255,255,255,0)_100%)]">
<Heading1 className="mb-4">
<div className="flex py-32 flex-col items-center w-full text-center bg-[#1F54FF] relative overflow-hidden">
{/* <div className="inset-0 absolute h-full w-full bg-[radial-gradient(circle,rgba(255,255,255,0.1)_0%,rgba(255,255,255,0)_100%)]"></div> */}
<div className="inset-0 absolute h-full w-full flex items-center justify-center">
<div className="w-[600px] h-[600px] ring-1 ring-white/05 rounded-full shrink-0"></div>
</div>
<div className="inset-0 absolute h-full w-full flex items-center justify-center">
<div className="w-[900px] h-[900px] ring-1 ring-white/10 rounded-full shrink-0"></div>
</div>
<div className="inset-0 absolute h-full w-full flex items-center justify-center">
<div className="w-[1200px] h-[1200px] ring-1 ring-white/20 rounded-full shrink-0"></div>
</div>
<div className="relative flex flex-col items-center max-w-3xl">
<Image
width={64}
height={64}
src="/logo-white.png"
alt="Openpanel Logo"
className="w-16 h-16 mb-8"
/>
<Heading1 className="mb-4 text-white">
An open-source
<br />
alternative to Mixpanel
</Heading1>
<p>
Mixpanel + Plausible ={' '}
<strong className="text-blue-600">Openpanel!</strong> A simple
analytics tool that your wallet can afford.
</p>
<Lead2 className="text-white/70 font-light">
Mixpanel + Plausible = <span className="text-white">Openpanel!</span>{' '}
<br />A simple analytics tool that your wallet can afford.
</Lead2>
<div className="my-12 w-full flex flex-col items-center">
<JoinWaitlist />
<div className="mt-2">
<p>{waitlistCount} people have already signed up! 🚀</p>
<JoinWaitlistHero />
<div className="mt-6 flex justify-center items-center">
<p className="text-white">
{waitlistCount} people have already signed up! 🚀
</p>
</div>
</div>
<div className="flex flex-wrap gap-10 max-w-xl justify-center">
{features.map(({ icon: Icon, title }) => (
<div className="flex gap-2 items-center justify-center">
<Icon className="text-blue-light " />
{title}
</div>
))}
</div>
</div>
<PreviewCarousel />
</div>
);
}

View File

@@ -0,0 +1,128 @@
'use client';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { cn } from '@/utils/cn';
import Link from 'next/link';
interface JoinWaitlistProps {
className?: string;
}
export function JoinWaitlistHero({ className }: JoinWaitlistProps) {
const [value, setValue] = useState('');
const [open, setOpen] = useState(false);
const [success, setSuccess] = useState(false);
useEffect(() => {
if (open) {
// @ts-ignore
window.openpanel.event('waitlist_open');
}
}, [open]);
useEffect(() => {
if (success) {
// @ts-ignore
window.openpanel.event('waitlist_success', {
email: value,
});
}
}, [success]);
const renderSuccess = () => (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Thanks so much!</DialogTitle>
<DialogDescription>
You're now on the waiting list. We'll let you know when we're ready.
Should be within a month or two 🚀
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setOpen(false)}>Got it!</Button>
</DialogFooter>
</DialogContent>
);
const renderForm = () => (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Almost there!</DialogTitle>
<DialogDescription>
Enter your email to join the waiting list. We'll let you know when
we're ready.
</DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
fetch('/api/waitlist', {
method: 'POST',
body: JSON.stringify({ email: value }),
headers: {
'Content-Type': 'application/json',
},
}).then((res) => {
if (res.ok) {
setSuccess(true);
}
});
}}
>
<div className="relative w-full">
<input
placeholder="Enter your email"
className={cn(
'border border-slate-100 rounded-md shadow-sm bg-white h-12 w-full px-4 outline-none focus:ring-1 ring-black text-blue-darker',
className
)}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<Button type="submit" className="absolute right-1 top-1">
Join waitlist
</Button>
</div>
</form>
</DialogContent>
);
return (
<>
<Dialog open={open} onOpenChange={setOpen}>
{success ? renderSuccess() : renderForm()}
</Dialog>
<div className="flex gap-4">
<Button
size="lg"
className="text-lg h-12"
onClick={() => {
setOpen(true);
}}
>
Join waitlist now
</Button>
<Link
href="https://dashboard.openpanel.dev/share/overview/ZQsEhG"
target="_blank"
rel="nofollow"
>
<Button size="lg" variant="outline" className="text-lg h-12">
Demo
</Button>
</Link>
</div>
</>
);
}

View File

@@ -2,7 +2,6 @@ import { cn } from '@/utils/cn';
import '@/styles/globals.css';
import { Logo } from '@/components/Logo';
import type { Metadata } from 'next';
import { Bricolage_Grotesque } from 'next/font/google';
import Script from 'next/script';
@@ -36,13 +35,6 @@ export default function RootLayout({
font.className
)}
>
<div className="absolute top-0 left-0 right-0 py-6 z-10">
<div className="container">
<div className="flex justify-between">
<Logo />
</div>
</div>
</div>
<div
className="w-full h-screen text-blue-950 bg-[radial-gradient(circle_at_2px_2px,#D9DEF6_2px,transparent_0)] absolute top-0 left-0 right-0 z-0"
style={{

View File

@@ -1,6 +1,6 @@
import type { Metadata } from 'next';
const title = 'Openpanel.dev | An open-source alternative to Mixpanel';
const title = 'An open-source alternative to Mixpanel | Openpanel.dev';
const description =
'Unlock actionable insights effortlessly with Insightful, the open-source analytics library that combines the power of Mixpanel with the simplicity of Plausible. Enjoy a unified overview, predictable pricing, and a vibrant community. Join us in democratizing analytics today!';

View File

@@ -13,13 +13,8 @@ export default async function Page() {
return (
<div>
<Hero waitlistCount={waitlistCount} />
<div className="bg-gradient-to-b from-blue-light to-[#FFFFFF] pb-16 text-center">
<div className="relative -top-20">
<PreviewCarousel />
</div>
</div>
<div className="container">
<div className="mb-24">
<div className="my-24">
<Heading2 className="md:text-5xl mb-2 leading-none">
Analytics should be easy
<br />

View File

@@ -10,29 +10,23 @@ import {
CheckCircle,
ClockIcon,
CloudIcon,
CloudLightning,
CloudLightningIcon,
CompassIcon,
ConeIcon,
DatabaseIcon,
DollarSignIcon,
DownloadIcon,
FileIcon,
FilterIcon,
FolderIcon,
FolderOpenIcon,
HandCoinsIcon,
HandshakeIcon,
KeyIcon,
PieChartIcon,
PointerIcon,
RouteIcon,
ServerIcon,
ShieldPlusIcon,
ShoppingCartIcon,
SquareUserRound,
StarIcon,
ThumbsUp,
ThumbsUpIcon,
TrendingUpIcon,
UserRoundSearchIcon,
@@ -40,23 +34,11 @@ import {
WebhookIcon,
} from 'lucide-react';
import {
Blob1,
Blob2,
Blob3,
Blob4,
Blob5,
Blob6,
Blob7,
Blob8,
Blob9,
} from './blob';
import { Heading2, Lead2 } from './copy';
import { Widget } from './widget';
interface SectionItem {
title: string;
description: string;
description: string | React.ReactNode;
icon: LucideIcon;
color: string;
soon?: string;
@@ -67,8 +49,18 @@ interface SectionItem {
const sections: SectionItem[] = [
{
title: 'Own Your Own Data',
description:
'Take control of your data privacy and ownership with our platform, ensuring full transparency and security.',
description: (
<>
<p>
Take control of your data privacy and ownership with our platform,
ensuring full transparency and security.
</p>
<p>
All our serveres are hosted in EU (Stockholm) and we are fully GDPR
compliant.
</p>
</>
),
icon: KeyIcon,
color: '#2563EB',
icons: [FolderIcon, DatabaseIcon, ShieldPlusIcon, KeyIcon],
@@ -76,8 +68,12 @@ const sections: SectionItem[] = [
},
{
title: 'Cloud or Self-Hosting',
description:
'Choose between the flexibility of cloud-based hosting or the autonomy of self-hosting to tailor your analytics infrastructure to your needs.',
description: (
<p>
Choose between the flexibility of cloud-based hosting or the autonomy of
self-hosting to tailor your analytics infrastructure to your needs.
</p>
),
icon: CloudIcon,
color: '#ff7557',
icons: [CloudIcon, CheckCircle, ServerIcon, DownloadIcon],
@@ -85,8 +81,12 @@ const sections: SectionItem[] = [
},
{
title: 'Real-Time Events',
description:
'Stay up-to-date with real-time event tracking, enabling instant insights into user actions as they happen.',
description: (
<p>
Stay up-to-date with real-time event tracking, enabling instant insights
into user actions as they happen.
</p>
),
icon: ClockIcon,
color: '#7fe1d8',
icons: [CloudLightningIcon, ShoppingCartIcon, ArrowUpFromDotIcon],
@@ -94,8 +94,13 @@ const sections: SectionItem[] = [
},
{
title: 'Deep Dive into User Behaviors',
description:
"Gain profound insights into user behavior with comprehensive analytics tools, allowing you to understand your audience's actions and preferences.",
description: (
<p>
Gain profound insights into user behavior with comprehensive analytics
tools, allowing you to understand your audience's actions and
preferences.
</p>
),
icon: UserRoundSearchIcon,
color: '#f8bc3c',
icons: [UsersIcon, RouteIcon, BookmarkIcon],
@@ -103,8 +108,12 @@ const sections: SectionItem[] = [
},
{
title: 'Powerful Report Explorer',
description:
'Explore and analyze your data effortlessly with our powerful report explorer, simplifying the process of deriving meaningful insights.',
description: (
<p>
Explore and analyze your data effortlessly with our powerful report
explorer, simplifying the process of deriving meaningful insights.
</p>
),
icon: CompassIcon,
color: '#b3596e',
icons: [ThumbsUpIcon, TrendingUpIcon, PieChartIcon, BarChart2Icon],
@@ -113,8 +122,12 @@ const sections: SectionItem[] = [
{
soon: 'Coming soon',
title: 'Funnels',
description:
'Track user conversion funnels seamlessly, providing valuable insights into user journey optimization.',
description: (
<p>
Track user conversion funnels seamlessly, providing valuable insights
into user journey optimization.
</p>
),
icon: ConeIcon,
color: '#72bef4',
icons: [ConeIcon, FilterIcon],
@@ -123,8 +136,13 @@ const sections: SectionItem[] = [
{
soon: 'Coming with our native app',
title: 'Push Notifications',
description:
'Stay informed about conversions, events, and peaks with our upcoming push notification tool, empowering you to monitor and respond to critical activities in real-time.',
description: (
<p>
Stay informed about conversions, events, and peaks with our upcoming
push notification tool, empowering you to monitor and respond to
critical activities in real-time.
</p>
),
icon: BellIcon,
color: '#ffb27a',
icons: [WebhookIcon, BellIcon],
@@ -132,8 +150,12 @@ const sections: SectionItem[] = [
},
{
title: 'Cost-Effective Alternative to Mixpanel',
description:
'Enjoy the same powerful analytics capabilities as Mixpanel at a fraction of the cost, ensuring affordability without compromising on quality.',
description: (
<p>
Enjoy the same powerful analytics capabilities as Mixpanel at a fraction
of the cost, ensuring affordability without compromising on quality.
</p>
),
icon: DollarSignIcon,
color: '#0f7ea0',
icons: [DollarSignIcon, HandCoinsIcon, HandshakeIcon, StarIcon],
@@ -142,8 +164,13 @@ const sections: SectionItem[] = [
{
soon: 'Something Plausible lacks',
title: 'Great Support for React Native',
description:
'Benefit from robust support for React Native, ensuring seamless integration and compatibility for your projects, a feature notably lacking in other platforms like Plausible.',
description: (
<p>
Benefit from robust support for React Native, ensuring seamless
integration and compatibility for your projects, a feature notably
lacking in other platforms like Plausible.
</p>
),
icon: (({ className }: LucideProps) => {
return (
<img src="/react-native.svg" alt="React Native" className={className} />
@@ -151,7 +178,7 @@ const sections: SectionItem[] = [
}) as unknown as LucideIcon,
color: '#3ba974',
icons: [FolderIcon, DatabaseIcon, ShieldPlusIcon, KeyIcon],
className: 'bg-[#f8bc3c]',
className: 'bg-[#e19900]',
},
];
@@ -187,7 +214,7 @@ export function Sections() {
: ['-top-10 -left-20 rotate-12', 'top-10 -rotate-12', '-right-5'];
const className = even
? cn('text-white [&_h3]:text-white col-span-2', section.className)
? cn('[&_*]:text-white/90 col-span-2', section.className)
: cn('border border-border', section.className);
return (
@@ -198,7 +225,7 @@ export function Sections() {
icons={section.icons}
offsets={offsets}
>
<p>{section.description}</p>
{section.description}
</Widget>
);
})}

View File

@@ -22,12 +22,12 @@ export function Widget({
return (
<div
className={cn(
'p-10 rounded-xl relative overflow-hidden flex flex-col hover:scale-105 transition-all duration-300 ease-in-out bg-white hover:shadow min-h-[300px] max-md:col-span-3',
'p-10 rounded-xl relative overflow-hidden flex flex-col hover:scale-[101%] transition-all duration-300 ease-in-out bg-white hover:shadow min-h-[300px] max-md:col-span-3',
className
)}
>
<Heading3 className="mb-4">{title}</Heading3>
{children}
<Heading3 className="mb-2">{title}</Heading3>
<div className="prose-xl">{children}</div>
<div className="flex justify-between mt-auto">
{icons.map((Icon, i) => (
<Icon