public: update landing page

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-03-19 22:37:37 +01:00
parent b7513f24d5
commit 1b9edc6bd2
47 changed files with 509 additions and 4061 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>React icon</title><path d="M12 9.861A2.139 2.139 0 1 0 12 14.139 2.139 2.139 0 1 0 12 9.861zM6.008 16.255l-.472-.12C2.018 15.246 0 13.737 0 11.996s2.018-3.25 5.536-4.139l.472-.119.133.468a23.53 23.53 0 0 0 1.363 3.578l.101.213-.101.213a23.307 23.307 0 0 0-1.363 3.578l-.133.467zM5.317 8.95c-2.674.751-4.315 1.9-4.315 3.046 0 1.145 1.641 2.294 4.315 3.046a24.95 24.95 0 0 1 1.182-3.046A24.752 24.752 0 0 1 5.317 8.95zM17.992 16.255l-.133-.469a23.357 23.357 0 0 0-1.364-3.577l-.101-.213.101-.213a23.42 23.42 0 0 0 1.364-3.578l.133-.468.473.119c3.517.889 5.535 2.398 5.535 4.14s-2.018 3.25-5.535 4.139l-.473.12zm-.491-4.259c.48 1.039.877 2.06 1.182 3.046 2.675-.752 4.315-1.901 4.315-3.046 0-1.146-1.641-2.294-4.315-3.046a24.788 24.788 0 0 1-1.182 3.046zM5.31 8.945l-.133-.467C4.188 4.992 4.488 2.494 6 1.622c1.483-.856 3.864.155 6.359 2.716l.34.349-.34.349a23.552 23.552 0 0 0-2.422 2.967l-.135.193-.235.02a23.657 23.657 0 0 0-3.785.61l-.472.119zm1.896-6.63c-.268 0-.505.058-.705.173-.994.573-1.17 2.565-.485 5.253a25.122 25.122 0 0 1 3.233-.501 24.847 24.847 0 0 1 2.052-2.544c-1.56-1.519-3.037-2.381-4.095-2.381zM16.795 22.677c-.001 0-.001 0 0 0-1.425 0-3.255-1.073-5.154-3.023l-.34-.349.34-.349a23.53 23.53 0 0 0 2.421-2.968l.135-.193.234-.02a23.63 23.63 0 0 0 3.787-.609l.472-.119.134.468c.987 3.484.688 5.983-.824 6.854a2.38 2.38 0 0 1-1.205.308zm-4.096-3.381c1.56 1.519 3.037 2.381 4.095 2.381h.001c.267 0 .505-.058.704-.173.994-.573 1.171-2.566.485-5.254a25.02 25.02 0 0 1-3.234.501 24.674 24.674 0 0 1-2.051 2.545zM18.69 8.945l-.472-.119a23.479 23.479 0 0 0-3.787-.61l-.234-.02-.135-.193a23.414 23.414 0 0 0-2.421-2.967l-.34-.349.34-.349C14.135 1.778 16.515.767 18 1.622c1.512.872 1.812 3.37.824 6.855l-.134.468zM14.75 7.24c1.142.104 2.227.273 3.234.501.686-2.688.509-4.68-.485-5.253-.988-.571-2.845.304-4.8 2.208A24.849 24.849 0 0 1 14.75 7.24zM7.206 22.677A2.38 2.38 0 0 1 6 22.369c-1.512-.871-1.812-3.369-.823-6.854l.132-.468.472.119c1.155.291 2.429.496 3.785.609l.235.02.134.193a23.596 23.596 0 0 0 2.422 2.968l.34.349-.34.349c-1.898 1.95-3.728 3.023-5.151 3.023zm-1.19-6.427c-.686 2.688-.509 4.681.485 5.254.987.563 2.843-.305 4.8-2.208a24.998 24.998 0 0 1-2.052-2.545 24.976 24.976 0 0 1-3.233-.501zM12 16.878c-.823 0-1.669-.036-2.516-.106l-.235-.02-.135-.193a30.388 30.388 0 0 1-1.35-2.122 30.354 30.354 0 0 1-1.166-2.228l-.1-.213.1-.213a30.3 30.3 0 0 1 1.166-2.228c.414-.716.869-1.43 1.35-2.122l.135-.193.235-.02a29.785 29.785 0 0 1 5.033 0l.234.02.134.193a30.006 30.006 0 0 1 2.517 4.35l.101.213-.101.213a29.6 29.6 0 0 1-2.517 4.35l-.134.193-.234.02c-.847.07-1.694.106-2.517.106zm-2.197-1.084c1.48.111 2.914.111 4.395 0a29.006 29.006 0 0 0 2.196-3.798 28.585 28.585 0 0 0-2.197-3.798 29.031 29.031 0 0 0-4.394 0 28.477 28.477 0 0 0-2.197 3.798 29.114 29.114 0 0 0 2.197 3.798z"/></svg>
<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>React icon</title><path d="M12 9.861A2.139 2.139 0 1 0 12 14.139 2.139 2.139 0 1 0 12 9.861zM6.008 16.255l-.472-.12C2.018 15.246 0 13.737 0 11.996s2.018-3.25 5.536-4.139l.472-.119.133.468a23.53 23.53 0 0 0 1.363 3.578l.101.213-.101.213a23.307 23.307 0 0 0-1.363 3.578l-.133.467zM5.317 8.95c-2.674.751-4.315 1.9-4.315 3.046 0 1.145 1.641 2.294 4.315 3.046a24.95 24.95 0 0 1 1.182-3.046A24.752 24.752 0 0 1 5.317 8.95zM17.992 16.255l-.133-.469a23.357 23.357 0 0 0-1.364-3.577l-.101-.213.101-.213a23.42 23.42 0 0 0 1.364-3.578l.133-.468.473.119c3.517.889 5.535 2.398 5.535 4.14s-2.018 3.25-5.535 4.139l-.473.12zm-.491-4.259c.48 1.039.877 2.06 1.182 3.046 2.675-.752 4.315-1.901 4.315-3.046 0-1.146-1.641-2.294-4.315-3.046a24.788 24.788 0 0 1-1.182 3.046zM5.31 8.945l-.133-.467C4.188 4.992 4.488 2.494 6 1.622c1.483-.856 3.864.155 6.359 2.716l.34.349-.34.349a23.552 23.552 0 0 0-2.422 2.967l-.135.193-.235.02a23.657 23.657 0 0 0-3.785.61l-.472.119zm1.896-6.63c-.268 0-.505.058-.705.173-.994.573-1.17 2.565-.485 5.253a25.122 25.122 0 0 1 3.233-.501 24.847 24.847 0 0 1 2.052-2.544c-1.56-1.519-3.037-2.381-4.095-2.381zM16.795 22.677c-.001 0-.001 0 0 0-1.425 0-3.255-1.073-5.154-3.023l-.34-.349.34-.349a23.53 23.53 0 0 0 2.421-2.968l.135-.193.234-.02a23.63 23.63 0 0 0 3.787-.609l.472-.119.134.468c.987 3.484.688 5.983-.824 6.854a2.38 2.38 0 0 1-1.205.308zm-4.096-3.381c1.56 1.519 3.037 2.381 4.095 2.381h.001c.267 0 .505-.058.704-.173.994-.573 1.171-2.566.485-5.254a25.02 25.02 0 0 1-3.234.501 24.674 24.674 0 0 1-2.051 2.545zM18.69 8.945l-.472-.119a23.479 23.479 0 0 0-3.787-.61l-.234-.02-.135-.193a23.414 23.414 0 0 0-2.421-2.967l-.34-.349.34-.349C14.135 1.778 16.515.767 18 1.622c1.512.872 1.812 3.37.824 6.855l-.134.468zM14.75 7.24c1.142.104 2.227.273 3.234.501.686-2.688.509-4.68-.485-5.253-.988-.571-2.845.304-4.8 2.208A24.849 24.849 0 0 1 14.75 7.24zM7.206 22.677A2.38 2.38 0 0 1 6 22.369c-1.512-.871-1.812-3.369-.823-6.854l.132-.468.472.119c1.155.291 2.429.496 3.785.609l.235.02.134.193a23.596 23.596 0 0 0 2.422 2.968l.34.349-.34.349c-1.898 1.95-3.728 3.023-5.151 3.023zm-1.19-6.427c-.686 2.688-.509 4.681.485 5.254.987.563 2.843-.305 4.8-2.208a24.998 24.998 0 0 1-2.052-2.545 24.976 24.976 0 0 1-3.233-.501zM12 16.878c-.823 0-1.669-.036-2.516-.106l-.235-.02-.135-.193a30.388 30.388 0 0 1-1.35-2.122 30.354 30.354 0 0 1-1.166-2.228l-.1-.213.1-.213a30.3 30.3 0 0 1 1.166-2.228c.414-.716.869-1.43 1.35-2.122l.135-.193.235-.02a29.785 29.785 0 0 1 5.033 0l.234.02.134.193a30.006 30.006 0 0 1 2.517 4.35l.101.213-.101.213a29.6 29.6 0 0 1-2.517 4.35l-.134.193-.234.02c-.847.07-1.694.106-2.517.106zm-2.197-1.084c1.48.111 2.914.111 4.395 0a29.006 29.006 0 0 0 2.196-3.798 28.585 28.585 0 0 0-2.197-3.798 29.031 29.031 0 0 0-4.394 0 28.477 28.477 0 0 0-2.197 3.798 29.114 29.114 0 0 0 2.197 3.798z"/></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -5,7 +5,7 @@ export const revalidate = 3600;
export default function Page() {
return (
<div className="container mt-[150px]">
<div className="container mt-[150px] max-w-2xl">
<article className="prose">
<Heading1>Privacy Policy</Heading1>
<p>Last updated: February 22, 2024</p>

View File

@@ -5,7 +5,7 @@ export const revalidate = 3600;
export default function Page() {
return (
<div className="container mt-[150px]">
<div className="container mt-[150px] max-w-2xl">
<article className="prose">
<Heading1>Terms and Conditions</Heading1>
<p>Last updated: February 22, 2024</p>

View File

@@ -1,74 +0,0 @@
import * as React from 'react';
import type { SVGProps } from 'react';
export const Blob1 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M162.9 81.7c6.8 18.6-7.8 46.3-29.5 61.3-21.6 15-50.3 17.4-70.1 3.8-19.8-13.5-30.8-42.9-23.2-62.7 7.6-19.7 33.7-29.9 60.9-30.2 27.1-.3 55.2 9.2 61.9 27.8Z" />
</svg>
);
export const Blob2 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M142.7 86.2c9.4 28.8 11.5 60-2.1 70-13.6 9.9-42.8-1.5-63.9-18.2-21.2-16.7-34.2-38.8-28.9-62C53 52.9 76.5 28.7 96.6 29.8c20.1 1.1 36.8 27.5 46.1 56.4Z" />
</svg>
);
export const Blob3 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M135.5 88.9c4.3 12.9-2.5 29.9-18.7 44-16.2 14-41.6 25.1-54.7 16.3-13.1-8.8-14-37.6-5.6-56.1C64.9 74.7 82.4 66.4 99 66.8c16.6.3 32.2 9.2 36.5 22.1Z" />
</svg>
);
export const Blob4 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M161.5 88.5c9.2 20.1 1.7 54.1-18.1 67.6-19.8 13.5-51.9 6.6-76.8-11.2-25-17.9-42.8-46.7-36-63.3 6.7-16.6 38.1-21 66.8-20.2 28.7.9 54.8 7 64.1 27.1Z" />
</svg>
);
export const Blob5 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M144.6 81.1c3.8 15.9-9.2 32.9-27.5 47.4-18.4 14.6-42.2 26.7-58 17.6-15.7-9-23.5-39.3-15.3-61.3 8.1-21.9 32.2-35.6 54.4-35 22.2.5 42.7 15.4 46.4 31.3Z" />
</svg>
);
export const Blob6 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M158 83.8c9.5 26.6 4.2 60.6-14.8 74.1-19.1 13.5-52 6.5-68.5-8.9-16.5-15.3-16.6-39-9.7-62 7-23.1 21-45.5 40.1-47.2 19.1-1.7 43.4 17.5 52.9 44Z" />
</svg>
);
export const Blob7 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M163.8 81.2c6.3 17.6-9.8 44.3-28.5 55.1-18.8 10.7-40.3 5.4-58.3-7.1C58.9 116.6 44.2 96.9 48.6 82c4.4-14.9 27.9-24.9 54-25.7 26.1-.9 54.9 7.4 61.2 24.9Z" />
</svg>
);
export const Blob8 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M140.2 93.2c8.6 20.4 10.1 49.3-2.1 57.8-12.2 8.6-38.2-3.3-54.2-17.6s-22.1-31-17.8-45.5c4.4-14.5 19.1-26.6 34.4-26.8 15.3-.2 31 11.7 39.7 32.1Z" />
</svg>
);
export const Blob9 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M150.6 84.9c6.7 19.2-2 44.7-22.1 60.8-20.1 16.2-51.8 23-73.6 8.9C33 140.5 21 105.4 30.1 82.8 39.2 60.2 69.6 50 95.8 51.4c26.2 1.3 48.1 14.3 54.8 33.5Z" />
</svg>
);
export const Blob10 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M162 75.8c6.7 24.7-7.5 52.1-30.7 69.5s-55.4 24.8-77.8 10.4C31 141.4 18.3 105.4 27.7 77 37 48.6 68.5 27.9 98.1 28.5c29.5.6 57.2 22.6 63.9 47.3Z" />
</svg>
);
export const Blob11 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M164.3 77.3c7.7 25.5-5.2 55.9-26.2 69.9-21 14.1-50.2 11.8-66.2-1.5-16.1-13.3-19-37.7-12.2-62 6.9-24.3 23.6-48.7 46.1-50.6 22.5-1.9 50.8 18.7 58.5 44.2Z" />
</svg>
);
export const Blob12 = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" {...props}>
<path d="M140 81.8c5.8 23.1.4 44.7-14.1 55.7-14.6 11-38.2 11.4-59-1.5C46 123.1 27.8 96.9 33.8 73.6c6-23.4 36.1-43.8 59.7-41.7 23.6 2.1 40.7 26.8 46.5 49.9Z" />
</svg>
);

View File

@@ -1,75 +0,0 @@
'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-2/1.png',
},
{
title: 'Histogram, perfect for showing active users',
url: '/demo-2/2.png',
},
{ title: 'Make your overview public', url: '/demo-2/3.png' },
{
title: 'See real time events from your users',
url: '/demo-2/4.png',
},
{ title: 'The classic line chart', url: '/demo-2/5.png' },
{
title: 'Bar charts to see your most popular content',
url: '/demo-2/6.png',
},
{ title: 'Get nice metric cards with graphs', url: '/demo-2/7.png' },
];
export function PreviewCarousel() {
return (
<Carousel
className="w-full"
opts={{ loop: true }}
plugins={[
Autoplay({
delay: 2000,
}),
]}
>
<CarouselContent>
{images.map((item) => (
<CarouselItem
key={item.url}
className="flex-[0_0_80%] max-w-3xl pl-8"
>
<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={2982 * 0.5}
height={1484 * 0.5}
alt={item.title}
/>
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="hidden md:visible" />
<CarouselNext className="hidden md:visible" />
</Carousel>
);
}

View File

@@ -27,7 +27,7 @@ export function Heading1({ children, className }: Props) {
return (
<h1
className={cn(
'text-4xl md:text-5xl font-bold text-slate-800 !leading-tight',
'text-4xl md:text-5xl font-bold text-slate-900 !leading-tight font-serif',
className
)}
>
@@ -39,7 +39,10 @@ export function Heading1({ children, className }: Props) {
export function Heading2({ children, className }: Props) {
return (
<h2
className={cn('text-4xl md:text-5xl font-bold text-slate-800', className)}
className={cn(
'text-4xl md:text-5xl font-bold text-slate-900 font-serif',
className
)}
>
{children}
</h2>
@@ -49,7 +52,23 @@ export function Heading2({ children, className }: Props) {
export function Heading3({ children, className }: Props) {
return (
<h3
className={cn('text-2xl md:text-3xl font-bold text-slate-800', className)}
className={cn(
'text-2xl md:text-3xl font-bold text-slate-900 font-serif',
className
)}
>
{children}
</h3>
);
}
export function Heading4({ children, className }: Props) {
return (
<h3
className={cn(
'text-xl md:text-2xl font-bold text-slate-900 font-serif',
className
)}
>
{children}
</h3>

View File

@@ -0,0 +1,145 @@
'use client';
import { cn } from '@/utils/cn';
import Image from 'next/image';
import { Heading3 } from './copy';
interface FeatureItem {
title: string;
description: string | React.ReactNode;
className: string;
image: string;
}
const features: FeatureItem[] = [
{
title: 'Visualize Your Data',
description: (
<p>
Gain a deep understanding of your data with our visualization tools.
</p>
),
className: '',
image: '/demo-3/img-1.png',
},
{
title: 'Get a good overview',
description: (
<p>
Even though we want to provide advanced charts and graphs, we also want
you to understand your data at a glance.
</p>
),
className: 'bg-slate-100',
image: '/demo-3/img-2.png',
},
{
title: 'Real-Time Data Access',
description: (
<>
<p>
Access all your events in real-time. No delays or waiting for data to
be accessible.
</p>
<p>
Mark events as conversions to highlight and soon notifications with
out iOS/Android app.
</p>
</>
),
className: '',
image: '/demo-3/img-3.png',
},
{
title: 'Unlimited dashboards with charts',
description: (
<>
<p>
Create beautiful charts and graphs to visualize your data and share
them with your team.
</p>
<div className="flex gap-2 flex-wrap">
<div className="border border-border px-3 py-1 rounded">
Linear
</div>
<div className="border border-border px-3 py-1 rounded"> Area</div>
<div className="border border-border px-3 py-1 rounded"> Bar</div>
<div className="border border-border px-3 py-1 rounded"> Map</div>
<div className="border border-border px-3 py-1 rounded"> Pie</div>
<div className="border border-border px-3 py-1 rounded">
Funnels
</div>
<div className="border border-border px-3 py-1 rounded">
Histogram
</div>
<div className="border border-border px-3 py-1 rounded">
Metrics
</div>
</div>
</>
),
className: 'bg-slate-100',
image: '/demo-3/img-4.png',
},
{
title: 'Understand your users',
description: (
<>
<p>
Deep dive into your user's behavior and understand how they interact
with your app/website.
</p>
</>
),
className: '',
image: '/demo-3/img-5.png',
},
];
export function Features() {
return (
<div className="flex flex-col">
{features.map((feature, i) => {
return (
<Feature key={feature.title} {...feature} even={i % 2 === 0}>
{feature.description}
</Feature>
);
})}
</div>
);
}
export function Feature({
title,
children,
className,
image,
even,
}: FeatureItem & { even: boolean; children: React.ReactNode }) {
return (
<section className={cn('py-16 group', className)}>
<div
className={cn(
'container flex min-h-[300px] items-center gap-16 justify-between max-md:flex-col-reverse',
!even && 'md:flex-row-reverse'
)}
>
<div className="flex flex-col w-full">
<Heading3 className="mb-2">{title}</Heading3>
<div className="prose-xl">{children}</div>
</div>
<div className="w-full">
<Image
src={image}
alt={title}
width={600}
height={400}
className="border-8 border-black/5 rounded-xl w-full max-w-xl group-hover:rotate-1 group-hover:scale-[101%] transition-transform duration-500"
/>
</div>
</div>
</section>
);
}

View File

@@ -1,56 +1,36 @@
import { Tooltip, TooltipContent } from '@/components/ui/tooltip';
import { TooltipTrigger } from '@radix-ui/react-tooltip';
import Image from 'next/image';
import { PreviewCarousel } from './carousel';
import { Heading1, Lead2 } from './copy';
import { JoinWaitlistHero } from './join-waitlist-hero';
import { SocialProofServer } from './social-proof';
import { SocialProof } from './social-proof/social-proof';
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() {
return (
<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>
<Lead2 className="text-white/70 font-light">
The power of Mixpanel, the ease of Plausible <br />
and nothing from Google Analytics 😉
</Lead2>
<div className="my-12 w-full flex flex-col items-center">
<div className="relative overflow-hidden">
{/* <div className="bg-blue-50 w-2/5 h-full absolute top-0 right-0"></div> */}
<div className="container md:h-screen min-h-[700px] flex flex-col md:flex-row items-center relative max-md:pt-32 gap-4 md:gap-8">
<div className="flex-1 lg:min-w-[400px] sm:min-w-[350px] max-md:text-center">
<Heading1 className="mb-4 text-slate-950">
An open-source
<br />
alternative to Mixpanel
</Heading1>
<Lead2 className="mb-12">
The power of Mixpanel, the ease of Plausible and nothing from Google
Analytics 😉
</Lead2>
<JoinWaitlistHero />
<SocialProofServer className="mt-6" />
</div>
<div className="mt-16 md:pt-8 w-full">
<div className="bg-black/5 md:p-2 rounded-2xl h-[max(90vh,650px)] flex">
<iframe
src="https://dashboard.openpanel.dev/share/overview/ZQsEhG"
className="w-full h-full rounded-xl h-[max(90vh,650px)]"
title="Openpanel Dashboard"
scrolling="no"
/>
</div>
</div>
</div>
<PreviewCarousel />
</div>
);
}

View File

@@ -11,7 +11,6 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { cn } from '@/utils/cn';
import Link from 'next/link';
interface JoinWaitlistProps {
className?: string;
@@ -20,51 +19,33 @@ interface JoinWaitlistProps {
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.op?.('event', 'waitlist_open');
window.op('event', 'waitlist_success');
}
}, [open]);
useEffect(() => {
if (success) {
// @ts-ignore
window.op?.('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>
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', {
@@ -75,7 +56,7 @@ export function JoinWaitlistHero({ className }: JoinWaitlistProps) {
},
}).then((res) => {
if (res.ok) {
setSuccess(true);
setOpen(true);
}
});
}}
@@ -95,42 +76,6 @@ export function JoinWaitlistHero({ className }: JoinWaitlistProps) {
</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>
<div className="relative">
<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>
<img
src="/clickable-demo.png"
className="w-44 shrink-0 absolute left-full -top-8 translate-x-10 max-w-none"
alt="Clickable demo button"
/>
</div>
</div>
</>
);
}

View File

@@ -1,6 +1,6 @@
import { cn } from '@/utils/cn';
import type { Metadata } from 'next';
import { Bricolage_Grotesque } from 'next/font/google';
import { Bricolage_Grotesque, Inter } from 'next/font/google';
import { OpenpanelProvider } from '@openpanel/nextjs';
@@ -9,6 +9,8 @@ import { defaultMeta } from './meta';
import '@/styles/globals.css';
import { Navbar } from './navbar';
export const metadata: Metadata = {
...defaultMeta,
alternates: {
@@ -16,10 +18,17 @@ export const metadata: Metadata = {
},
};
const font = Bricolage_Grotesque({
const head = Bricolage_Grotesque({
display: 'swap',
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-serif',
});
const body = Inter({
display: 'swap',
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-sans',
});
export default function RootLayout({
@@ -31,10 +40,12 @@ export default function RootLayout({
<html lang="en" className="light">
<body
className={cn(
'min-h-screen antialiased grainy text-slate-600',
font.className
'min-h-screen antialiased grainy text-slate-900 font-sans',
head.variable,
body.variable
)}
>
<Navbar darkText />
{children}
<Footer />
</body>

View File

@@ -9,6 +9,8 @@ export const defaultMeta: Metadata = {
description,
openGraph: {
title,
url: 'https://openpanel.dev',
type: 'website',
images: [
{
url: 'https://openpanel.dev/ogimage.png',

View File

@@ -18,7 +18,7 @@ export function Navbar({ darkText = false, className }: Props) {
className={cn('absolute top-0 left-0 right-0 z-10', textColor, className)}
>
<div className="container flex justify-between items-center py-4">
<Logo />
<Logo className="max-sm:[&_span]:hidden" />
<nav className="flex gap-4">
{pathname !== '/' && <Link href="/">Home</Link>}
<a href="https://docs.openpanel.dev" target="_blank">

View File

@@ -1,9 +1,11 @@
import { db } from '@openpanel/db';
import { ALink } from '@/components/ui/button';
import { ExternalLinkIcon } from 'lucide-react';
import { Heading2, Lead2, Paragraph } from './copy';
import { Features } from './features';
import { Hero } from './hero';
import { Navbar } from './navbar';
import { Sections } from './section';
import { Pricing } from './pricing';
import { PunchLines } from './punch-lines';
export const dynamic = 'force-dynamic';
export const revalidate = 3600;
@@ -11,22 +13,36 @@ export const revalidate = 3600;
export default function Page() {
return (
<div>
<Navbar darkText={false} className="[&_img]:hidden" />
<Hero />
<div className="container">
<div className="my-24">
<Heading2 className="md:text-5xl mb-2 leading-none">
<div className="py-24 bg-gradient-to-bl from-blue-600 to-blue-800">
<div className="container">
<Heading2 className="md:text-5xl mb-2 leading-none text-white">
Analytics should be easy
<br />
and powerful
</Heading2>
<Lead2>
<Lead2 className="text-white/80">
The power of Mixpanel, the ease of Plausible and nothing from Google
Analytics 😉
Analytics 😉 Curious how it looks?
</Lead2>
<ALink
href="https://dashboard.openpanel.dev/share/overview/ZQsEhG"
target="_blank"
className="mt-8"
variant={'outline'}
>
Check out the demo
<ExternalLinkIcon className="w-4 h-4 ml-2" />
</ALink>
</div>
<Sections />
</div>
<Features />
<PunchLines />
<Pricing />
<div className="container mt-40">
<div className="flex flex-col md:flex-row gap-8">
<div className="mb-4 md:w-1/2 flex-shrink-0 relative">

View File

@@ -0,0 +1,72 @@
import { CheckIcon } from 'lucide-react';
import { Heading2, Lead2 } from './copy';
export function Pricing() {
return (
<div className="bg-slate-200 py-32" id="#pricing">
<div className="container">
<section className="container flex flex-col gap-6 md:max-w-[64rem]">
<div className="mx-auto flex w-full flex-col gap-4 md:max-w-[58rem]">
<Heading2>Simple, transparent pricing</Heading2>
<Lead2 className="max-w-[85%] leading-normal text-muted-foreground sm:text-lg sm:leading-7">
Everything is included, just decide how many events you want to
track each month.
</Lead2>
</div>
<div className="grid w-full items-start gap-10 rounded-lg border md:p-10 md:grid-cols-[1fr_200px]">
<div className="grid gap-6">
<h3 className="text-xl font-bold sm:text-2xl">
What&apos;s included for{' '}
<span className="bg-slate-300 rounded px-0.5">all plans</span>
</h3>
<ul className="grid gap-3 text-sm text-muted-foreground sm:grid-cols-2">
<li className="flex items-center">
<CheckIcon className="mr-2 h-4 w-4" /> Unlimited websites/apps
</li>
<li className="flex items-center">
<CheckIcon className="mr-2 h-4 w-4" /> Unlimited Users
</li>
<li className="flex items-center">
<CheckIcon className="mr-2 h-4 w-4" /> Unlimted dashboards
</li>
<li className="flex items-center">
<CheckIcon className="mr-2 h-4 w-4" /> Unlimted charts
</li>
<li className="flex items-center">
<CheckIcon className="mr-2 h-4 w-4" /> Unlimted tracked
profiles
</li>
<li className="flex items-center font-bold text-slate-900">
<CheckIcon className="mr-2 h-4 w-4" /> Yes, its that simple
</li>
</ul>
</div>
<div className="flex flex-col gap-4 text-left md:text-right">
<div>
<p className="text-sm font-medium text-muted-foreground">
From
</p>
<h4 className="text-7xl font-bold">$10</h4>
<p className="text-sm font-medium text-muted-foreground">
billed monthly
</p>
</div>
{/* <Link href="/login" className={cn(buttonVariants({ size: "lg" }))}>
Get Started
</Link> */}
</div>
</div>
<div className="mx-auto flex w-full max-w-[58rem] flex-col gap-4">
<p className="max-w-[85%] leading-normal text-muted-foreground sm:leading-7">
Exact pricing will come soon, but we asure you, it will be cheaper
than the competition.
</p>
<p className="font-bold">During beta everything is free!</p>
</div>
</section>
</div>
</div>
);
}

View File

@@ -0,0 +1,141 @@
import { cn } from '@/utils/cn';
import type { LucideIcon, LucideProps } from 'lucide-react';
import {
ClockIcon,
CloudIcon,
CookieIcon,
DollarSignIcon,
HandshakeIcon,
KeyIcon,
} from 'lucide-react';
import Image from 'next/image';
import { Heading2, Heading3, Heading4 } from './copy';
const items = [
{
title: 'Own Your Own Data',
description: (
<p>
All our serveres are hosted in EU (Stockholm) and we are fully GDPR
compliant.
</p>
),
icon: KeyIcon,
color: '#2563EB',
className: 'bg-blue-light',
},
{
title: 'Cloud or Self-Hosting',
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',
className: '', // 'bg-[#ff7557]',
},
{
title: 'Real-Time Events',
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',
className: '', // bg-[#7fe1d8]
},
{
title: 'No cookies!',
description: (
<p>
Our trackers are cookie-free, skip that annyoing cookie consent banner!
</p>
),
icon: CookieIcon,
color: '#f8bc3c',
className: 'bg-blue-dark', //'bg-[#f8bc3c]',
},
{
title: 'Cost-Effective',
description: (
<p>
We have combined the best from Mixpanel and Plausible. Cut the costs and
keep the features.
</p>
),
icon: DollarSignIcon,
color: '#0f7ea0',
className: 'bg-[#3ba974]',
},
{
title: 'Predictable pricing',
description: (
<p>You only pay for events, everything else is included. No surprises.</p>
),
icon: HandshakeIcon,
color: '#0f7ea0',
className: 'bg-[#3ba974]',
},
{
title: 'First Class React Native Support',
description: (
<p>
Our SDK is built with React Native in mind, making it easy to integrate
with your mobile apps.
</p>
),
icon: (({ className }: LucideProps) => {
return (
<Image
src="/react-native.svg"
alt="React Native"
className={cn(className, 'p-3')}
width={50}
height={50}
/>
);
}) as unknown as LucideIcon,
color: '#3ba974',
className: 'bg-[#e19900]',
},
];
export function PunchLines() {
return (
<div className="bg-slate-700 py-32">
<Heading2 className="text-white text-center mb-16">
Not convinced?
</Heading2>
<div className="container">
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{items.map((item) => {
const Icon = item.icon;
return (
<div
className="border border-border p-6 rounded-xl bg-white"
key={item.title}
>
<div
className={cn(
'h-14 w-14 rounded-full flex items-center justify-center mb-4',
item.color
)}
style={{ background: item.color }}
>
<Icon color="#fff" />
</div>
<Heading4>{item.title}</Heading4>
<div className="prose">{item.description}</div>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -1,235 +0,0 @@
'use client';
import { cn } from '@/utils/cn';
import type { LucideIcon, LucideProps } from 'lucide-react';
import {
ArrowUpFromDotIcon,
BarChart2Icon,
BellIcon,
BookmarkIcon,
CheckCircle,
ClockIcon,
CloudIcon,
CloudLightningIcon,
CompassIcon,
ConeIcon,
DatabaseIcon,
DollarSignIcon,
DownloadIcon,
FilterIcon,
FolderIcon,
HandCoinsIcon,
HandshakeIcon,
KeyIcon,
PieChartIcon,
RouteIcon,
ServerIcon,
ShieldPlusIcon,
ShoppingCartIcon,
StarIcon,
ThumbsUpIcon,
TrendingUpIcon,
UserRoundSearchIcon,
UsersIcon,
WebhookIcon,
} from 'lucide-react';
import { Widget } from './widget';
interface SectionItem {
title: string;
description: string | React.ReactNode;
icon: LucideIcon;
color: string;
soon?: string;
icons: LucideIcon[];
className: string;
}
const sections: SectionItem[] = [
{
title: 'Own Your Own Data',
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],
className: 'bg-blue-light',
},
{
title: 'Cloud or Self-Hosting',
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],
className: '', // 'bg-[#ff7557]',
},
{
title: 'Real-Time Events',
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],
className: '', // bg-[#7fe1d8]
},
{
title: 'Deep Dive into User Behaviors',
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],
className: 'bg-blue-dark', //'bg-[#f8bc3c]',
},
{
title: 'Powerful Report Explorer',
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],
className: 'bg-[#ff7557]',
},
{
soon: 'Coming soon',
title: 'Funnels',
description: (
<p>
Track user conversion funnels seamlessly, providing valuable insights
into user journey optimization.
</p>
),
icon: ConeIcon,
color: '#72bef4',
icons: [ConeIcon, FilterIcon],
className: '', //'bg-[#72bef4]',
},
{
soon: 'Coming with our native app',
title: 'Push Notifications',
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],
className: '', //'bg-[#ffb27a]',
},
{
title: 'Cost-Effective Alternative to Mixpanel',
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],
className: 'bg-[#3ba974]',
},
{
soon: 'Something Plausible lacks',
title: 'Great Support for React Native',
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} />
);
}) as unknown as LucideIcon,
color: '#3ba974',
icons: [FolderIcon, DatabaseIcon, ShieldPlusIcon, KeyIcon],
className: 'bg-[#e19900]',
},
];
// To lazy to think now...
function checkIndex(index: number) {
switch (index) {
case 0:
case 3:
case 4:
case 7:
case 8:
case 10:
return true;
default:
return false;
}
}
export function Sections() {
return (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 gap-y-16">
{sections.map((section, i) => {
const even = checkIndex(i);
const offsets = even
? [
'-top-10 -left-10 rotate-12',
'top-10 -rotate-12',
'-right-5',
'-right-10 -top-20',
]
: ['-top-10 -left-20 rotate-12', 'top-10 -rotate-12', '-right-5'];
const className = even
? cn('[&_*]:text-white/90 col-span-2', section.className)
: cn('border border-border', section.className);
return (
<Widget
key={section.title}
title={section.title}
className={className}
icons={section.icons}
offsets={offsets}
>
{section.description}
</Widget>
);
})}
</div>
</>
);
}

View File

@@ -11,7 +11,7 @@ export async function SocialProofServer(props: Props) {
const waitlistCount = await db.waitlist.count();
return (
<TooltipProvider>
<SocialProof count={waitlistCount} {...props} />;
<SocialProof count={waitlistCount} {...props} />
</TooltipProvider>
);
}

View File

@@ -1,21 +1,12 @@
'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 {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/utils/cn';
// import { StarIcon } from 'lucide-react';
import Image from 'next/image';
interface JoinWaitlistProps {
@@ -25,15 +16,15 @@ interface JoinWaitlistProps {
export function SocialProof({ className, count }: JoinWaitlistProps) {
return (
<div className={cn('flex gap-2 justify-center items-center', className)}>
<div className={cn('flex items-center gap-2', className)}>
<div className="flex">
<Tooltip>
<TooltipTrigger>
<Image
className="rounded-full"
src="/clickhouse.png"
width={24}
height={24}
width={40}
height={40}
alt="Clickhouse"
/>
</TooltipTrigger>
@@ -44,8 +35,8 @@ export function SocialProof({ className, count }: JoinWaitlistProps) {
<Image
className="rounded-full"
src="/getdreams.png"
width={24}
height={24}
width={40}
height={40}
alt="GetDreams"
/>
</TooltipTrigger>
@@ -56,17 +47,27 @@ export function SocialProof({ className, count }: JoinWaitlistProps) {
<Image
className="rounded-full"
src="/kiddokitchen.png"
width={24}
height={24}
width={40}
height={40}
alt="KiddoKitchen"
/>
</TooltipTrigger>
<TooltipContent>KiddoKitchen is here</TooltipContent>
</Tooltip>
</div>
<p className="text-white">
{count} early birds have already signed up! 🚀
</p>
<div>
<p className="text-left">{count} early birds have signed up! 🚀</p>
{/* <div className="flex gap-0.5">
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
<StarIcon size={16} color="#F5C962" fill="#F5C962" />
</div> */}
</div>
</div>
);
}
// <div class="flex flex-col gap-y-2 mt-5 lg:mt-3"><p class="text-gray-700 dark:text-gray-100 text-xs w-24 text-start font-semibold whitespace-nowrap">What users think</p><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"></div><img class="w-5 h-5 rounded-full " src="https://pbs.twimg.com/profile_images/1744063824431370240/BbVtyCiy_normal.png" alt="feedback_0"><span class="flex-wrap text-xs text-gray-600 dark:text-gray-200 text-start font-normal">“ Been a long time Mixpanel user and without a doubt there's a bunch of room to innovate. I'm confident Openpanel is on the right path! ”</span></div><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame w-5 h-5 text-red-600 fill-orange-400 animate-pulse"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg></div><img class="w-5 h-5 rounded-full border border-2 border-red-500" src="https://pbs.twimg.com/profile_images/1751607056316944384/8E4F88FL_normal.jpg" alt="feedback_1"><span class="flex-wrap text-xs text-gray-600 dark:text-gray-200 text-start font-normal">“ I have used Openpanel for the last 6 months (since Im the creator) for 3 different sites/apps. Its a great analytics product that has everything you need. Still lacking a native app but will work hard to make that a reality! ”</span></div><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"></div><img class="w-5 h-5 rounded-full " src="https://pbs.twimg.com/profile_images/1701887174042324992/g2GBIQay_normal.jpg" alt="feedback_2"><span class="flex-wrap text-xs text-gray-600 dark:text-gray-200 text-start font-normal">“ would be cool if it was easier to edit text after image is generated ”</span></div><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flame w-5 h-5 text-red-600 fill-orange-400 animate-pulse"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg></div><img class="w-5 h-5 rounded-full border border-2 border-red-500" src="https://pbs.twimg.com/profile_images/1194368464946974728/1D2biimN_normal.jpg" alt="feedback_3"><span class="flex-wrap text-xs text-gray-600 dark:text-gray-200 text-start font-normal">“ Awesome product, very easy to use and understand. I miss a native app and the documentation could be improved. Otherwise I love it. ”</span></div><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"></div><img class="w-5 h-5 rounded-full " src="https://lh3.googleusercontent.com/a/ACg8ocIWiGTd3nWE5etp-CFhxrTKFvSLSJJd7pPmiM9SNJ9sAg=s96-c" alt="feedback_4"><span class="flex-wrap text-xs text-gray-600 dark:text-gray-200 text-start font-normal">“ I have used Open panel since the private beta and i'm super impressed by the product already, the speed after you give feedback to actually get the features is truly amazing! Can't wait to see where Openpanel are in 6 months! ”</span></div><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"></div><img class="w-5 h-5 rounded-full " src="https://lh3.googleusercontent.com/a/ACg8ocKymAw_YoIrfoGp-bWMlDsXgM6St0dzaVJ7m_lGNXDtrA=s96-c" alt="feedback_5"><span class="flex-wrap text-xs text-gray-600 dark:text-gray-200 text-start font-normal">“ Impressively fast UI and easy to integrate! Added it alongside my current analytics tool for my native app in less than an hour. ”</span></div><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"></div><img class="w-5 h-5 rounded-full " src="https://pbs.twimg.com/profile_images/1735771119980879872/Mx5MlB9e_normal.jpg" alt="feedback_6"><span class="flex-wrap text-xs text-gray-600 dark:text-gray-200 text-start font-normal">“ Im using plausible and find it pleasing but limited.
// Im looking forward to trying out Openpanel, the demo pictures and the page look professional. The listed features seem to be broader then plausible. :) ”</span></div><div class="flex flex-row w-full gap-x-2 items-start"><div class="w-5 shrink-0"></div><img class="w-5 h-5 rounded-full " src="https://pbs.twimg.com/profile_images/1767459527006334976/unbMENPG_normal.jpg" alt="feedback_7"><span class="flex-wrap text-xs text-gray-600 dark:text-gray-200 text-start font-normal">“ Incredibly easy to implement and a joy to use. 5/5 would recommend. ”</span></div></div>

View File

@@ -1,43 +0,0 @@
import type { ReactNode } from 'react';
import { cn } from '@/utils/cn';
import type { LucideIcon } from 'lucide-react';
import { Heading3 } from './copy';
interface WidgetProps {
title: string;
children: ReactNode;
className?: string;
icons: LucideIcon[];
offsets: string[];
}
export function Widget({
title,
children,
className,
icons,
offsets,
}: WidgetProps) {
return (
<div
className={cn(
'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-2">{title}</Heading3>
<div className="prose-xl">{children}</div>
<div className="flex justify-between mt-auto">
{icons.map((Icon, i) => (
<Icon
key={i}
size={120}
className={cn('flex-shrink-0 opacity-5 relative', offsets?.[i])}
strokeWidth={1.5}
/>
))}
</div>
</div>
);
}

View File

@@ -19,7 +19,7 @@ export function Logo({ className }: LogoProps) {
width={32}
height={32}
/>
openpanel.dev
<span>openpanel.dev</span>
</Link>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +0,0 @@
'use client';
import { cn } from '@/utils/cn';
import { Asterisk, ChevronRight } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
interface RenderDotsProps extends React.HTMLAttributes<HTMLDivElement> {
children: string;
truncate?: boolean;
}
export function RenderDots({
children,
className,
truncate,
...props
}: RenderDotsProps) {
const parts = children.split('.');
const sliceAt = truncate && parts.length > 3 ? 3 : 0;
return (
<Tooltip
disableHoverableContent={true}
open={sliceAt === 0 ? false : undefined}
>
<TooltipTrigger>
<div {...props} className={cn('flex items-center gap-1', className)}>
{parts.slice(-sliceAt).map((str, index) => {
return (
<div className="flex items-center gap-1" key={str + index}>
{index !== 0 && (
<ChevronRight className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" />
)}
{str.includes('[*]') ? (
<>
{str.replace('[*]', '')}
<Asterisk className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" />
</>
) : str === '*' ? (
<Asterisk className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" />
) : (
str
)}
</div>
);
})}
</div>
</TooltipTrigger>
<TooltipContent align="start">
<p>{children}</p>
</TooltipContent>
</Tooltip>
);
}

View File

@@ -1,140 +0,0 @@
'use client';
import * as React from 'react';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/utils/cn';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full',
className
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
className
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@@ -1,59 +0,0 @@
import * as React from 'react';
import { cn } from '@/utils/cn';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = 'Alert';
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
));
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
));
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,7 +0,0 @@
'use client';
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };

View File

@@ -1,49 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full text-sm',
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-primary text-white',
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,40 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-1.5 h-[20px] text-[10px] font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
success:
'border-transparent bg-emerald-500 text-emerald-100 hover:bg-emerald-500/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@@ -84,3 +84,16 @@ Button.defaultProps = {
};
export { Button, buttonVariants };
export interface ALinkProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
VariantProps<typeof buttonVariants> {}
export const ALink = ({ variant, size, className, ...props }: ALinkProps) => {
return (
<a
{...props}
className={cn(buttonVariants({ variant, size, className }))}
/>
);
};

View File

@@ -1,29 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -1,122 +0,0 @@
'use client';
import * as React from 'react';
import { Badge } from '@/components/ui/badge';
import { Command, CommandGroup, CommandItem } from '@/components/ui/command';
import { useOnClickOutside } from 'usehooks-ts';
import { Checkbox } from './checkbox';
import { Input } from './input';
type IValue = any;
type IItem = Record<'value' | 'label', IValue>;
interface ComboboxAdvancedProps {
value: IValue[];
onChange: React.Dispatch<React.SetStateAction<IValue[]>>;
items: IItem[];
placeholder: string;
}
export function ComboboxAdvanced({
items,
value,
onChange,
placeholder,
}: ComboboxAdvancedProps) {
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState('');
const ref = React.useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, () => setOpen(false));
const selectables = items
.filter((item) => !value.find((s) => s === item.value))
.filter(
(item) =>
(typeof item.label === 'string' &&
item.label.toLowerCase().includes(inputValue.toLowerCase())) ||
(typeof item.value === 'string' &&
item.value.toLowerCase().includes(inputValue.toLowerCase()))
);
const renderItem = (item: IItem) => {
const checked = !!value.find((s) => s === item.value);
return (
<CommandItem
key={String(item.value)}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={() => {
setInputValue('');
onChange((prev) => {
if (prev.includes(item.value)) {
return prev.filter((s) => s !== item.value);
}
return [...prev, item.value];
});
}}
className={'cursor-pointer flex items-center gap-2'}
>
<Checkbox checked={checked} className="pointer-events-none" />
{item?.label ?? item?.value}
</CommandItem>
);
};
const renderUnknownItem = (value: IValue) => {
const item = items.find((item) => item.value === value);
return item ? renderItem(item) : renderItem({ value, label: value });
};
return (
<Command className="overflow-visible bg-white" ref={ref}>
<button
type="button"
className="group border border-input px-3 py-2 text-sm ring-offset-background rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
onClick={() => setOpen((prev) => !prev)}
>
<div className="flex gap-1 flex-wrap">
{value.length === 0 && placeholder}
{value.slice(0, 2).map((value) => {
const item = items.find((item) => item.value === value) ?? {
value,
label: value,
};
return (
<Badge key={String(item.value)} variant="secondary">
{item.label}
</Badge>
);
})}
{value.length > 2 && (
<Badge variant="secondary">+{value.length - 2} more</Badge>
)}
</div>
</button>
{open && (
<div className="relative top-2">
<div className="max-h-80 min-w-[300px] absolute w-full z-10 top-0 rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
<CommandGroup className="max-h-80 overflow-auto">
<div className="p-1 mb-2">
<Input
placeholder="Type to search"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
/>
</div>
{inputValue === ''
? value.map(renderUnknownItem)
: renderItem({
value: inputValue,
label: `Pick "${inputValue}"`,
})}
{selectables.map(renderItem)}
</CommandGroup>
</div>
</div>
)}
</Command>
);
}

View File

@@ -1,125 +0,0 @@
'use client';
import * as React from 'react';
import { Badge } from '@/components/ui/badge';
import { Command, CommandGroup, CommandItem } from '@/components/ui/command';
import { Command as CommandPrimitive } from 'cmdk';
import { X } from 'lucide-react';
type Item = Record<'value' | 'label', string>;
interface ComboboxMultiProps {
selected: Item[];
setSelected: React.Dispatch<React.SetStateAction<Item[]>>;
items: Item[];
placeholder: string;
}
export function ComboboxMulti({
items,
selected,
setSelected,
placeholder,
...props
}: ComboboxMultiProps) {
const inputRef = React.useRef<HTMLInputElement>(null);
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState('');
const handleUnselect = React.useCallback((item: Item) => {
setSelected((prev) => prev.filter((s) => s.value !== item.value));
}, []);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current;
if (input) {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (input.value === '') {
setSelected((prev) => {
const newSelected = [...prev];
newSelected.pop();
return newSelected;
});
}
}
// This is not a default behaviour of the <input /> field
if (e.key === 'Escape') {
input.blur();
}
}
},
[]
);
const selectables = items.filter(
(item) => !selected.find((s) => s.value === item.value)
);
return (
<Command onKeyDown={handleKeyDown} className="overflow-visible bg-white">
<div className="group border border-input px-3 py-2 text-sm ring-offset-background rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
<div className="flex gap-1 flex-wrap">
{selected.map((item) => {
return (
<Badge key={item.value} variant="secondary">
{item.label}
<button
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUnselect(item);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(item)}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
);
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
ref={inputRef}
value={inputValue}
onValueChange={setInputValue}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
placeholder={placeholder}
className="ml-2 bg-transparent outline-none placeholder:text-muted-foreground flex-1"
/>
</div>
</div>
<div className="relative mt-2">
{open && selectables.length > 0 ? (
<div className="absolute w-full z-10 top-0 rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
<CommandGroup className="h-full overflow-auto">
{selectables.map((item) => {
return (
<CommandItem
key={item.value}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={(value) => {
setInputValue('');
setSelected((prev) => [...prev, item]);
}}
className={'cursor-pointer'}
>
{item.label}
</CommandItem>
);
})}
</CommandGroup>
</div>
) : null}
</div>
</Command>
);
}

View File

@@ -1,139 +0,0 @@
'use client';
import * as React from 'react';
import type { ButtonProps } from '@/components/ui/button';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@/components/ui/command';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/utils/cn';
import type { LucideIcon } from 'lucide-react';
import { Check, ChevronsUpDown } from 'lucide-react';
export interface ComboboxProps<T> {
placeholder: string;
items: {
value: T;
label: string;
disabled?: boolean;
}[];
value: T | null | undefined;
onChange: (value: T) => void;
children?: React.ReactNode;
onCreate?: (value: T) => void;
className?: string;
searchable?: boolean;
icon?: LucideIcon;
size?: ButtonProps['size'];
label?: string;
}
export type ExtendedComboboxProps<T> = Omit<
ComboboxProps<T>,
'items' | 'placeholder'
> & {
placeholder?: string;
};
export function Combobox<T extends string>({
placeholder,
items,
value,
onChange,
children,
onCreate,
className,
searchable,
icon: Icon,
size,
label,
}: ComboboxProps<T>) {
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState('');
function find(value: string) {
return items.find(
(item) => item.value.toLowerCase() === value.toLowerCase()
);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{children ?? (
<Button
size={size}
variant="outline"
role="combobox"
aria-expanded={open}
className={cn('justify-between', className)}
>
{Icon ? <Icon className="mr-2" size={16} /> : null}
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
{value ? find(value)?.label ?? 'No match' : placeholder}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
)}
</PopoverTrigger>
<PopoverContent className="w-full max-w-md p-0" align="start">
<Command>
{searchable === true && (
<CommandInput
placeholder="Search item..."
value={search}
onValueChange={setSearch}
/>
)}
{typeof onCreate === 'function' && search ? (
<CommandEmpty className="p-2">
<Button
onClick={() => {
onCreate(search as T);
setSearch('');
setOpen(false);
}}
>
Create &quot;{search}&quot;
</Button>
</CommandEmpty>
) : (
<CommandEmpty>Nothing selected</CommandEmpty>
)}
<div className="max-h-[300px] overflow-y-auto over-x-hidden">
<CommandGroup>
{items.map((item) => (
<CommandItem
key={item.value}
value={item.value}
onSelect={(currentValue) => {
const value = find(currentValue)?.value ?? currentValue;
onChange(value as T);
setOpen(false);
}}
{...(item.disabled && { disabled: true })}
>
<Check
className={cn(
'mr-2 h-4 w-4 flex-shrink-0',
value === item.value ? 'opacity-100' : 'opacity-0'
)}
/>
{item.label}
</CommandItem>
))}
</CommandGroup>
</div>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -1,155 +0,0 @@
'use client';
import * as React from 'react';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { cn } from '@/utils/cn';
import type { DialogProps } from '@radix-ui/react-dialog';
import { Command as CommandPrimitive } from 'cmdk';
import { Search } from 'lucide-react';
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn('-mx-1 h-px bg-border', className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
data-disabled={props.disabled}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = 'CommandShortcut';
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@@ -1,199 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:mr-2',
inset && 'pl-8',
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@@ -1,26 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 block mb-2'
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@@ -1,47 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
export type RadioGroupProps = React.InputHTMLAttributes<HTMLDivElement>;
export type RadioGroupItemProps =
React.InputHTMLAttributes<HTMLButtonElement> & {
active?: boolean;
};
const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
({ className, type, ...props }, ref) => {
return (
<div
className={cn(
'flex h-10 divide-x rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
const RadioGroupItem = React.forwardRef<HTMLButtonElement, RadioGroupItemProps>(
({ className, active, ...props }, ref) => {
return (
<button
{...props}
className={cn(
'flex-1 px-3 whitespace-nowrap leading-none hover:bg-slate-100 transition-colors font-medium',
className,
active && 'bg-slate-100'
)}
type="button"
ref={ref}
/>
);
}
);
RadioGroup.displayName = 'RadioGroup';
RadioGroupItem.displayName = 'RadioGroupItem';
export { RadioGroup, RadioGroupItem };

View File

@@ -1,52 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
className={cn(
'relative rounded-full bg-border',
orientation === 'vertical' && 'flex-1'
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View File

@@ -1,143 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { ScrollArea } from '@radix-ui/react-scroll-area';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-150 data-[state=open]:duration-150',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay className="backdrop-blur-none bg-transparent" />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<div className="h-screen p-6 overflow-y-auto overflow-x-hidden">
{children}
</div>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -1,126 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement> & {
wrapper?: boolean;
overflow?: boolean;
}
>(({ className, wrapper, overflow = true, ...props }, ref) => (
<div className={cn('border border-border rounded-md bg-white', className)}>
<div className={cn('relative w-full', overflow && 'overflow-auto')}>
<table
ref={ref}
className={cn(
'w-full caption-bottom text-sm [&.mini]:text-xs',
className
)}
{...props}
/>
</div>
</div>
));
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={className} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn('bg-primary font-medium text-primary-foreground', className)}
{...props}
/>
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className
)}
{...props}
/>
));
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'p-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 shadow-[0_0_0_0.5px] shadow-border [.mini_&]:p-2',
className
)}
{...props}
/>
));
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
'p-4 align-middle [&:has([role=checkbox])]:pr-0 shadow-[0_0_0_0.5px] shadow-border [.mini_&]:p-2 whitespace-nowrap',
className
)}
{...props}
/>
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props}
/>
));
TableCaption.displayName = 'TableCaption';
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@@ -1,129 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -1,35 +0,0 @@
'use client';
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from '@/components/ui/toast';
import { useToast } from '@/components/ui/use-toast';
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -7,6 +7,10 @@ const config = {
'./src/**/*.{ts,tsx}',
],
theme: {
fontFamily: {
sans: ['var(--font-sans)'],
serif: ['var(--font-serif)'],
},
container: {
center: true,
padding: '1rem',