add public website

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-04 21:35:17 +01:00
parent ccd1a1456f
commit fab3a0d9a8
72 changed files with 4012 additions and 48 deletions

View File

@@ -0,0 +1,28 @@
import * as EmailValidator from 'email-validator';
// true
import { NextResponse } from 'next/server';
import { db } from '@mixan/db';
EmailValidator.validate('test@email.com');
export async function POST(req: Request) {
const body = await req.json();
if (!body.email) {
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
}
if (!EmailValidator.validate(body.email)) {
return NextResponse.json({ error: 'Email is not valid' }, { status: 400 });
}
await db.waitlist.create({
data: {
email: body.email.toLowerCase(),
},
});
return NextResponse.json(body);
}

View File

@@ -0,0 +1,72 @@
'use client';
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '@/components/ui/carousel';
import Autoplay from 'embla-carousel-autoplay';
import Image from 'next/image';
const images = [
{
title: 'Beautiful overview, everything is clickable to get more details',
url: '/demo/overview-min.png',
},
{
title: 'Histogram, perfect for showing active users',
url: '/demo/histogram-min.png',
},
{ title: 'Make your overview public', url: '/demo/overview-share-min.png' },
{
title: 'See real time events from your users',
url: '/demo/events-min.png',
},
{ title: 'The classic line chart', url: '/demo/line-min.png' },
{
title: 'Bar charts to see your most popular content',
url: '/demo/bar-min.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' },
];
export function HomeCarousel() {
return (
<div className="mx-auto max-w-6xl p-4">
<div className="relative">
<div className="rounded-lg w-full max-w-6xl aspect-video dashed absolute -left-5 -top-5"></div>
<Carousel
className="w-full"
opts={{ loop: true }}
plugins={[
Autoplay({
delay: 2000,
}),
]}
>
<CarouselContent>
{images.map((item) => (
<CarouselItem key={item.url}>
<div className="aspect-video rounded-md overflow-hidden">
<Image
className="w-full h-full object-cover"
src={item.url}
width={1080}
height={608}
alt={item.title}
/>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="hidden md:visible" />
<CarouselNext className="hidden md:visible" />
</Carousel>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { cn } from '@/utils/cn';
interface Props {
children: React.ReactNode;
className?: string;
}
export function Lead({ children, className }: Props) {
return (
<p className={cn('text-xl md:text-2xl font-light', className)}>
{children}
</p>
);
}
export function Paragraph({ children, className }: Props) {
return <p className={cn('text-lg', className)}>{children}</p>;
}
export function Heading1({ children, className }: Props) {
return (
<h1 className={cn('text-4xl md:text-6xl font-bold', className)}>
{children}
</h1>
);
}
export function Heading2({ children, className }: Props) {
return (
<h2 className={cn('text-2xl md:text-4xl font-bold', className)}>
{children}
</h2>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,65 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
export function JoinWaitlist() {
const [value, setValue] = useState('');
const [open, setOpen] = useState(false);
return (
<>
<Dialog open={open} onOpenChange={setOpen}>
<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>
</Dialog>
<form
className="w-full max-w-md"
onSubmit={(e) => {
e.preventDefault();
fetch('/api/waitlist', {
method: 'POST',
body: JSON.stringify({ email: value }),
headers: {
'Content-Type': 'application/json',
},
}).then((res) => {
if (res.ok) {
setOpen(true);
}
});
}}
>
<div className="relative w-full mb-8">
<input
placeholder="Enter your email"
className="border border-slate-100 rounded-md shadow-sm bg-white h-12 w-full px-4 outline-none focus:ring-1 ring-black"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<Button type="submit" className="absolute right-1 top-1">
Join waitlist
</Button>
</div>
</form>
</>
);
}

View File

@@ -0,0 +1,28 @@
import { cn } from '@/utils/cn';
import '@/styles/globals.css';
import type { Metadata } from 'next';
import { defaultMeta } from './meta';
export const metadata: Metadata = {
...defaultMeta,
alternates: {
canonical: 'https://openpanel.dev',
},
};
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="light">
<body className={cn('min-h-screen font-sans antialiased grainy')}>
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,22 @@
import type { MetadataRoute } from 'next';
import { defaultMeta } from './meta';
export default function manifest(): MetadataRoute.Manifest {
return {
name: defaultMeta.title as string,
short_name: 'Openpanel.dev',
description: defaultMeta.description!,
start_url: '/',
display: 'standalone',
background_color: '#fff',
theme_color: '#fff',
icons: [
{
src: '/favicon.ico',
sizes: 'any',
type: 'image/x-icon',
},
],
};
}

View File

@@ -0,0 +1,7 @@
import type { Metadata } from 'next';
export const defaultMeta: Metadata = {
title: 'Openpanel.dev | A Open-Source Analytics Library',
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

@@ -0,0 +1,158 @@
import { Logo } from '@/components/Logo';
import { Button } from '@/components/ui/button';
import {
BarChart2,
CookieIcon,
Globe2Icon,
LayoutPanelTopIcon,
LockIcon,
UsersIcon,
} from 'lucide-react';
import { HomeCarousel } from './carousel';
import { Heading1, Heading2, Lead, Paragraph } from './copy';
import { JoinWaitlist } from './join-waitlist';
const features = [
{
title: 'Great overview',
icon: LayoutPanelTopIcon,
},
{
title: 'Beautiful charts',
icon: BarChart2,
},
{
title: 'Privacy focused',
icon: LockIcon,
},
{
title: 'Open-source',
icon: Globe2Icon,
},
{
title: 'No cookies',
icon: CookieIcon,
},
{
title: 'User journey',
icon: UsersIcon,
},
];
export default function Page() {
return (
<div>
<div className="max-w-6xl p-4 mx-auto absolute top-0 left-0 right-0 py-6">
<div className="flex justify-between">
<Logo />
</div>
</div>
<div className="flex flex-col items-center bg-gradient-to-br from-white via-white to-blue-200 w-full text-center text-blue-950">
<div className="py-20 pt-32 p-4 flex flex-col items-center max-w-3xl ">
<Heading1 className="mb-4 fancy-text">
A open-source
<br />
alternative to Mixpanel
</Heading1>
<Lead className="mb-8">
Combine Mixpanel and Plausible and you get Openpanel. A simple
analytics tool that respects privacy.
</Lead>
<JoinWaitlist />
<div className="grid grid-cols-2 md:grid-cols-3 gap-8 mt-8">
{features.map(({ icon: Icon, title }) => (
<div className="flex gap-2 items-center justify-center">
<Icon />
{title}
</div>
))}
</div>
</div>
</div>
<div className="bg-blue-800 p-4 py-8 md:py-16 text-center">
<Heading2 className="text-slate-100 mb-4">
Get a feel how it looks
</Heading2>
<Lead className="text-slate-200 mb-16">
We've crafted a clean and intuitive interface because analytics should
<br />
be straightforward, unlike the complexity often associated with Google
Analytics. 😅
</Lead>
<HomeCarousel />
</div>
<div className="p-4 py-8 md:py-16 text-center flex flex-col items-center">
<Heading2 className="mb-4">Another analytic tool?</Heading2>
<div className="flex flex-col gap-4 max-w-3xl">
<Paragraph>
<strong>TL;DR</strong> Our open-source analytic library fills a
crucial gap by combining the strengths of Mixpanel's powerful
features with Plausible's clear overview page. Motivated by the lack
of an open-source alternative to Mixpanel and inspired by
Plausible's simplicity, we aim to create an intuitive platform with
predictable pricing. With a single-tier pricing model and limits
only on monthly event counts, our goal is to democratize analytics,
offering unrestricted access to all features while ensuring
affordability and transparency for users of all project sizes.
</Paragraph>
<div className="flex gap-2 w-full justify-center my-8">
<div className="rounded-full h-2 w-10 bg-blue-200"></div>
<div className="rounded-full h-2 w-10 bg-blue-400"></div>
<div className="rounded-full h-2 w-10 bg-blue-600"></div>
<div className="rounded-full h-2 w-10 bg-blue-800"></div>
</div>
<Paragraph>
Our open-source analytic library emerged from a clear need within
the analytics community. While platforms like Mixpanel offer
powerful and user-friendly features, they lack a comprehensive
overview page akin to Plausible's, which succinctly summarizes
essential metrics. Recognizing this gap, we saw an opportunity to
combine the strengths of both platforms while addressing their
respective shortcomings.
</Paragraph>
<Paragraph>
One significant motivation behind our endeavor was the absence of an
open-source alternative to Mixpanel. We believe in the importance of
accessibility and transparency in analytics, which led us to embark
on creating a solution that anyone can freely use and contribute to.
</Paragraph>
<Paragraph>
Inspired by Plausible's exemplary approach to simplicity and
clarity, we aim to build upon their foundation and further refine
the user experience. By harnessing the best practices demonstrated
by Plausible, we aspire to create an intuitive and streamlined
analytics platform that empowers users to derive actionable insights
effortlessly.
</Paragraph>
<Paragraph>
Our own experiences with traditional analytics platforms like
Mixpanel underscored another critical aspect driving our project:
the need for predictable pricing. As project owners ourselves, we
encountered the frustration of escalating costs as our user base
grew. Therefore, we are committed to offering a single-tier pricing
model that provides unlimited access to all features without the
fear of unexpected expenses.
</Paragraph>
<Paragraph>
In line with our commitment to fairness and accessibility, our
pricing model will only impose limits on the number of events users
can send each month. This approach, akin to Plausible's, ensures
that users have the freedom to explore and utilize our platform to
its fullest potential without arbitrary restrictions on reports or
user counts. Ultimately, our goal is to democratize analytics by
offering a reliable, transparent, and cost-effective solution for
projects of all sizes.
</Paragraph>
</div>
</div>
</div>
);
}