public: update landing page
BIN
apps/public/public/demo-3/img-1.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
apps/public/public/demo-3/img-2.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/public/public/demo-3/img-3.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
apps/public/public/demo-3/img-4.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
apps/public/public/demo-3/img-5.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
145
apps/public/src/app/features.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,6 +9,8 @@ export const defaultMeta: Metadata = {
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
url: 'https://openpanel.dev',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://openpanel.dev/ogimage.png',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
72
apps/public/src/app/pricing.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
141
apps/public/src/app/punch-lines.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 I’m the creator) for 3 different sites/apps. It’s 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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function Logo({ className }: LogoProps) {
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
openpanel.dev
|
||||
<span>openpanel.dev</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -1,7 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root;
|
||||
|
||||
export { AspectRatio };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 }))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 "{search}"
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,10 @@ const config = {
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['var(--font-sans)'],
|
||||
serif: ['var(--font-serif)'],
|
||||
},
|
||||
container: {
|
||||
center: true,
|
||||
padding: '1rem',
|
||||
|
||||