add public website
This commit is contained in:
28
apps/public/src/app/api/waitlist/route.ts
Normal file
28
apps/public/src/app/api/waitlist/route.ts
Normal 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);
|
||||
}
|
||||
72
apps/public/src/app/carousel.tsx
Normal file
72
apps/public/src/app/carousel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
apps/public/src/app/copy.tsx
Normal file
34
apps/public/src/app/copy.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
apps/public/src/app/favicon.ico
Normal file
BIN
apps/public/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
65
apps/public/src/app/join-waitlist.tsx
Normal file
65
apps/public/src/app/join-waitlist.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
apps/public/src/app/layout.tsx
Normal file
28
apps/public/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
apps/public/src/app/manifest.ts
Normal file
22
apps/public/src/app/manifest.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
7
apps/public/src/app/meta.ts
Normal file
7
apps/public/src/app/meta.ts
Normal 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!',
|
||||
};
|
||||
158
apps/public/src/app/page.tsx
Normal file
158
apps/public/src/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user