update public web
This commit is contained in:
@@ -34,11 +34,8 @@ const images = [
|
|||||||
{ title: 'The classic pie chart', url: '/demo/pie-min.png' },
|
{ title: 'The classic pie chart', url: '/demo/pie-min.png' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function HomeCarousel() {
|
export function PreviewCarousel() {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-6xl p-4">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="rounded-lg w-full max-w-6xl aspect-video dashed absolute -left-5 -top-5"></div>
|
|
||||||
<Carousel
|
<Carousel
|
||||||
className="w-full"
|
className="w-full"
|
||||||
opts={{ loop: true }}
|
opts={{ loop: true }}
|
||||||
@@ -50,23 +47,23 @@ export function HomeCarousel() {
|
|||||||
>
|
>
|
||||||
<CarouselContent>
|
<CarouselContent>
|
||||||
{images.map((item) => (
|
{images.map((item) => (
|
||||||
<CarouselItem key={item.url}>
|
<CarouselItem key={item.url} className="flex-[0_0_80%] pl-8">
|
||||||
<div className="aspect-video rounded-md overflow-hidden">
|
<div className="aspect-video">
|
||||||
|
<div className="p-3 bg-white/20 rounded-xl overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover rounded-lg"
|
||||||
src={item.url}
|
src={item.url}
|
||||||
width={1080}
|
width={1080}
|
||||||
height={608}
|
height={608}
|
||||||
alt={item.title}
|
alt={item.title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CarouselItem>
|
</CarouselItem>
|
||||||
))}
|
))}
|
||||||
</CarouselContent>
|
</CarouselContent>
|
||||||
<CarouselPrevious className="hidden md:visible" />
|
<CarouselPrevious className="hidden md:visible" />
|
||||||
<CarouselNext className="hidden md:visible" />
|
<CarouselNext className="hidden md:visible" />
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export function Paragraph({ children, className }: Props) {
|
|||||||
|
|
||||||
export function Heading1({ children, className }: Props) {
|
export function Heading1({ children, className }: Props) {
|
||||||
return (
|
return (
|
||||||
<h1 className={cn('text-4xl md:text-6xl font-bold', className)}>
|
<h1
|
||||||
|
className={cn('text-5xl md:text-6xl font-bold text-slate-800', className)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</h1>
|
</h1>
|
||||||
);
|
);
|
||||||
@@ -33,8 +35,20 @@ export function Heading1({ children, className }: Props) {
|
|||||||
|
|
||||||
export function Heading2({ children, className }: Props) {
|
export function Heading2({ children, className }: Props) {
|
||||||
return (
|
return (
|
||||||
<h2 className={cn('text-2xl md:text-4xl font-bold', className)}>
|
<h2
|
||||||
|
className={cn('text-4xl md:text-5xl font-bold text-slate-800', className)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Heading3({ children, className }: Props) {
|
||||||
|
return (
|
||||||
|
<h3
|
||||||
|
className={cn('text-2xl md:text-3xl font-bold text-slate-800', className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
79
apps/public/src/app/hero.tsx
Normal file
79
apps/public/src/app/hero.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// background-image: radial-gradient(circle at 1px 1px, black 1px, transparent 0);
|
||||||
|
// background-size: 40px 40px;
|
||||||
|
|
||||||
|
import { Logo } from '@/components/Logo';
|
||||||
|
import {
|
||||||
|
BarChart2Icon,
|
||||||
|
CookieIcon,
|
||||||
|
Globe2Icon,
|
||||||
|
LayoutPanelTopIcon,
|
||||||
|
LockIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import { Heading1, Lead, Lead2 } from './copy';
|
||||||
|
import { JoinWaitlist } from './join-waitlist';
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
title: 'Great overview',
|
||||||
|
icon: LayoutPanelTopIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Beautiful charts',
|
||||||
|
icon: BarChart2Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Privacy focused',
|
||||||
|
icon: LockIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Open-source',
|
||||||
|
icon: Globe2Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'No cookies',
|
||||||
|
icon: CookieIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="absolute top-0 left-0 right-0 py-6">
|
||||||
|
<div className="container">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center w-full text-center text-blue-950 bg-[radial-gradient(circle_at_2px_2px,#D9DEF6_2px,transparent_0)]"
|
||||||
|
style={{
|
||||||
|
backgroundSize: '70px 70px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="py-20 pt-32 p-4 flex flex-col items-center max-w-3xl bg-[radial-gradient(circle,rgba(255,255,255,0.7)_0%,rgba(255,255,255,0.7)_50%,rgba(255,255,255,0)_100%)]">
|
||||||
|
<Heading1 className="mb-4">
|
||||||
|
A open-source
|
||||||
|
<br />
|
||||||
|
alternative to Mixpanel
|
||||||
|
</Heading1>
|
||||||
|
<p className="mb-8">
|
||||||
|
Combine Mixpanel and Plausible and you get Openpanel. A simple
|
||||||
|
analytics tool that respects privacy.
|
||||||
|
</p>
|
||||||
|
<JoinWaitlist />
|
||||||
|
<div className="flex flex-wrap gap-10 mt-8 max-w-xl justify-center">
|
||||||
|
{features.map(({ icon: Icon, title }) => (
|
||||||
|
<div className="flex gap-2 items-center justify-center">
|
||||||
|
<Icon className="text-blue-light " />
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,8 +10,13 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
export function JoinWaitlist() {
|
interface JoinWaitlistProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JoinWaitlist({ className }: JoinWaitlistProps) {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@@ -59,7 +64,10 @@ export function JoinWaitlist() {
|
|||||||
<div className="relative w-full mb-8">
|
<div className="relative w-full mb-8">
|
||||||
<input
|
<input
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
className="border border-slate-100 rounded-md shadow-sm bg-white h-12 w-full px-4 outline-none focus:ring-1 ring-black"
|
className={cn(
|
||||||
|
'border border-slate-100 rounded-md shadow-sm bg-white h-12 w-full px-4 outline-none focus:ring-1 ring-black text-blue-darker',
|
||||||
|
className
|
||||||
|
)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { cn } from '@/utils/cn';
|
|||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { Bricolage_Grotesque } from 'next/font/google';
|
||||||
import Script from 'next/script';
|
import Script from 'next/script';
|
||||||
|
|
||||||
import { defaultMeta } from './meta';
|
import { defaultMeta } from './meta';
|
||||||
@@ -14,6 +15,12 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const font = Bricolage_Grotesque({
|
||||||
|
display: 'swap',
|
||||||
|
subsets: ['latin'],
|
||||||
|
weights: [400, 700],
|
||||||
|
});
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -21,7 +28,12 @@ export default async function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="light">
|
<html lang="en" className="light">
|
||||||
<body className={cn('min-h-screen font-sans antialiased grainy')}>
|
<body
|
||||||
|
className={cn(
|
||||||
|
'min-h-screen antialiased grainy text-slate-600',
|
||||||
|
font.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
<Script
|
<Script
|
||||||
|
|||||||
@@ -1,121 +1,58 @@
|
|||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/Logo';
|
||||||
import { Button } from '@/components/ui/button';
|
import Image from 'next/image';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
|
||||||
import {
|
|
||||||
BarChart2,
|
|
||||||
BellIcon,
|
|
||||||
ClockIcon,
|
|
||||||
CloudIcon,
|
|
||||||
CompassIcon,
|
|
||||||
ConeIcon,
|
|
||||||
CookieIcon,
|
|
||||||
DollarSignIcon,
|
|
||||||
Globe2Icon,
|
|
||||||
KeyIcon,
|
|
||||||
LayoutPanelTopIcon,
|
|
||||||
LockIcon,
|
|
||||||
UserRoundSearchIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
import { HomeCarousel } from './carousel';
|
import { PreviewCarousel } from './carousel';
|
||||||
import { Heading1, Heading2, Lead, Lead2, Paragraph } from './copy';
|
import { Heading2, Lead2, Paragraph } from './copy';
|
||||||
|
import { Hero } from './hero';
|
||||||
import { JoinWaitlist } from './join-waitlist';
|
import { JoinWaitlist } from './join-waitlist';
|
||||||
import { Section, Sections } from './section';
|
import { Sections } from './section';
|
||||||
|
|
||||||
const features = [
|
|
||||||
{
|
|
||||||
title: 'Great overview',
|
|
||||||
icon: LayoutPanelTopIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Beautiful charts',
|
|
||||||
icon: BarChart2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Privacy focused',
|
|
||||||
icon: LockIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Open-source',
|
|
||||||
icon: Globe2Icon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'No cookies',
|
|
||||||
icon: CookieIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'User journey',
|
|
||||||
icon: UsersIcon,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="max-w-6xl p-4 mx-auto absolute top-0 left-0 right-0 py-6">
|
<Hero />
|
||||||
<div className="flex justify-between">
|
<div className="bg-gradient-to-b from-blue-light to-[#FFFFFF] py-16 md:py-40 text-center">
|
||||||
<Logo />
|
<PreviewCarousel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="container">
|
||||||
|
<div className="mb-24">
|
||||||
<div className="flex flex-col items-center bg-gradient-to-br from-white via-white to-blue-200 w-full text-center text-blue-950">
|
<Heading2 className="md:text-5xl mb-2 leading-none">
|
||||||
<div className="py-20 pt-32 p-4 flex flex-col items-center max-w-3xl ">
|
Analytics should be easy
|
||||||
<Heading1 className="mb-4 fancy-text">
|
|
||||||
A open-source
|
|
||||||
<br />
|
<br />
|
||||||
alternative to Mixpanel
|
and powerful
|
||||||
</Heading1>
|
</Heading2>
|
||||||
<Lead className="mb-8">
|
<Lead2>
|
||||||
Combine Mixpanel and Plausible and you get Openpanel. A simple
|
The power of Mixpanel, the ease of Plausible and nothing from Google
|
||||||
analytics tool that respects privacy.
|
Analytics 😉
|
||||||
</Lead>
|
</Lead2>
|
||||||
<JoinWaitlist />
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-8 mt-8">
|
|
||||||
{features.map(({ icon: Icon, title }) => (
|
|
||||||
<div className="flex gap-2 items-center justify-center">
|
|
||||||
<Icon />
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Sections />
|
<Sections />
|
||||||
<div className="bg-blue-800 p-4 py-8 md:py-16 text-center">
|
|
||||||
<Heading2 className="text-slate-100 mb-4">
|
|
||||||
Get a feel how it looks
|
|
||||||
</Heading2>
|
|
||||||
<Lead className="text-slate-200 mb-16">
|
|
||||||
We've crafted a clean and intuitive interface because analytics should
|
|
||||||
<br />
|
|
||||||
be straightforward, unlike the complexity often associated with Google
|
|
||||||
Analytics. 😅
|
|
||||||
</Lead>
|
|
||||||
<HomeCarousel />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 py-8 md:py-16 text-center flex flex-col items-center">
|
<div className="container mt-40">
|
||||||
<Heading2 className="mb-4">Another analytic tool?</Heading2>
|
<div className="flex flex-col md:flex-row gap-8">
|
||||||
|
<div className="mb-4 md:w-1/2 flex-shrink-0 relative">
|
||||||
|
<Heading2>Another analytic tool? Really?</Heading2>
|
||||||
|
{/* <SirenIcon
|
||||||
|
strokeWidth={0.5}
|
||||||
|
size={300}
|
||||||
|
className="opacity-10 absolute -rotate-12 -left-20 -top-10"
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-4 max-w-3xl">
|
<div className="flex flex-col gap-4 max-w-3xl">
|
||||||
|
<h3 className="text-lg font-bold text-blue-dark">TL;DR</h3>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
<strong>TL;DR</strong> Our open-source analytic library fills a
|
Our open-source analytic library fills a crucial gap by combining
|
||||||
crucial gap by combining the strengths of Mixpanel's powerful
|
the strengths of Mixpanel's powerful features with Plausible's
|
||||||
features with Plausible's clear overview page. Motivated by the lack
|
clear overview page. Motivated by the lack of an open-source
|
||||||
of an open-source alternative to Mixpanel and inspired by
|
alternative to Mixpanel and inspired by Plausible's simplicity, we
|
||||||
Plausible's simplicity, we aim to create an intuitive platform with
|
aim to create an intuitive platform with predictable pricing. With
|
||||||
predictable pricing. With a single-tier pricing model and limits
|
a single-tier pricing model and limits only on monthly event
|
||||||
only on monthly event counts, our goal is to democratize analytics,
|
counts, our goal is to democratize analytics, offering
|
||||||
offering unrestricted access to all features while ensuring
|
unrestricted access to all features while ensuring affordability
|
||||||
affordability and transparency for users of all project sizes.
|
and transparency for users of all project sizes.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<div className="flex gap-2 w-full justify-center my-8">
|
<h3 className="text-lg font-bold text-blue-dark mt-12">The why</h3>
|
||||||
<div className="rounded-full h-2 w-10 bg-blue-200"></div>
|
|
||||||
<div className="rounded-full h-2 w-10 bg-blue-400"></div>
|
|
||||||
<div className="rounded-full h-2 w-10 bg-blue-600"></div>
|
|
||||||
<div className="rounded-full h-2 w-10 bg-blue-800"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
Our open-source analytic library emerged from a clear need within
|
Our open-source analytic library emerged from a clear need within
|
||||||
the analytics community. While platforms like Mixpanel offer
|
the analytics community. While platforms like Mixpanel offer
|
||||||
@@ -127,10 +64,11 @@ export default function Page() {
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
One significant motivation behind our endeavor was the absence of an
|
One significant motivation behind our endeavor was the absence of
|
||||||
open-source alternative to Mixpanel. We believe in the importance of
|
an open-source alternative to Mixpanel. We believe in the
|
||||||
accessibility and transparency in analytics, which led us to embark
|
importance of accessibility and transparency in analytics, which
|
||||||
on creating a solution that anyone can freely use and contribute to.
|
led us to embark on creating a solution that anyone can freely use
|
||||||
|
and contribute to.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
@@ -138,8 +76,8 @@ export default function Page() {
|
|||||||
clarity, we aim to build upon their foundation and further refine
|
clarity, we aim to build upon their foundation and further refine
|
||||||
the user experience. By harnessing the best practices demonstrated
|
the user experience. By harnessing the best practices demonstrated
|
||||||
by Plausible, we aspire to create an intuitive and streamlined
|
by Plausible, we aspire to create an intuitive and streamlined
|
||||||
analytics platform that empowers users to derive actionable insights
|
analytics platform that empowers users to derive actionable
|
||||||
effortlessly.
|
insights effortlessly.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
@@ -147,23 +85,70 @@ export default function Page() {
|
|||||||
Mixpanel underscored another critical aspect driving our project:
|
Mixpanel underscored another critical aspect driving our project:
|
||||||
the need for predictable pricing. As project owners ourselves, we
|
the need for predictable pricing. As project owners ourselves, we
|
||||||
encountered the frustration of escalating costs as our user base
|
encountered the frustration of escalating costs as our user base
|
||||||
grew. Therefore, we are committed to offering a single-tier pricing
|
grew. Therefore, we are committed to offering a single-tier
|
||||||
model that provides unlimited access to all features without the
|
pricing model that provides unlimited access to all features
|
||||||
fear of unexpected expenses.
|
without the fear of unexpected expenses.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
In line with our commitment to fairness and accessibility, our
|
In line with our commitment to fairness and accessibility, our
|
||||||
pricing model will only impose limits on the number of events users
|
pricing model will only impose limits on the number of events
|
||||||
can send each month. This approach, akin to Plausible's, ensures
|
users can send each month. This approach, akin to Plausible's,
|
||||||
that users have the freedom to explore and utilize our platform to
|
ensures that users have the freedom to explore and utilize our
|
||||||
its fullest potential without arbitrary restrictions on reports or
|
platform to its fullest potential without arbitrary restrictions
|
||||||
user counts. Ultimately, our goal is to democratize analytics by
|
on reports or user counts. Ultimately, our goal is to democratize
|
||||||
offering a reliable, transparent, and cost-effective solution for
|
analytics by offering a reliable, transparent, and cost-effective
|
||||||
projects of all sizes.
|
solution for projects of all sizes.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer className="bg-blue-darker text-white relative mt-40 relative">
|
||||||
|
<div className="inset-0 absolute h-full w-full bg-[radial-gradient(circle,rgba(255,255,255,0.2)_0%,rgba(255,255,255,0)_100%)]"></div>
|
||||||
|
<div className="relative container flex flex-col items-center text-center">
|
||||||
|
<div className="my-24">
|
||||||
|
<Heading2 className="text-white mb-2">Get early access</Heading2>
|
||||||
|
<Lead2>
|
||||||
|
Ready to set your analytics free? Get on our waitlist.
|
||||||
|
</Lead2>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<JoinWaitlist className="text-white bg-white/20 border-white/30 focus:ring-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-xl">
|
||||||
|
<div className="p-3 bg-white/20">
|
||||||
|
<Image
|
||||||
|
src="/demo/overview-min.png"
|
||||||
|
width={1080}
|
||||||
|
height={608}
|
||||||
|
alt="Openpanel overview page"
|
||||||
|
className="w-full rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0">
|
||||||
|
<div className="h-px w-full bg-[radial-gradient(circle,rgba(255,255,255,0.7)_0%,rgba(255,255,255,0.7)_50%,rgba(255,255,255,0)_100%)]"></div>
|
||||||
|
<div className="p-4 bg-blue-darker">
|
||||||
|
<div className="container">
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<Logo />
|
||||||
|
<a
|
||||||
|
className="hover:underline"
|
||||||
|
href="https://twitter.com/CarlLindesvard"
|
||||||
|
target="_blank"
|
||||||
|
rel="nofollow"
|
||||||
|
>
|
||||||
|
Follow on X
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,41 @@
|
|||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
|
ArrowUpFromDotIcon,
|
||||||
|
BarChart2Icon,
|
||||||
BellIcon,
|
BellIcon,
|
||||||
|
BookmarkIcon,
|
||||||
|
CheckCircle,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
CloudIcon,
|
CloudIcon,
|
||||||
|
CloudLightning,
|
||||||
|
CloudLightningIcon,
|
||||||
CompassIcon,
|
CompassIcon,
|
||||||
ConeIcon,
|
ConeIcon,
|
||||||
|
DatabaseIcon,
|
||||||
DollarSignIcon,
|
DollarSignIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
FileIcon,
|
||||||
|
FilterIcon,
|
||||||
|
FolderIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
HandCoinsIcon,
|
||||||
|
HandshakeIcon,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
|
PieChartIcon,
|
||||||
|
PointerIcon,
|
||||||
|
RouteIcon,
|
||||||
|
ServerIcon,
|
||||||
|
ShieldPlusIcon,
|
||||||
|
ShoppingCartIcon,
|
||||||
|
SquareUserRound,
|
||||||
|
StarIcon,
|
||||||
|
ThumbsUp,
|
||||||
|
ThumbsUpIcon,
|
||||||
|
TrendingUpIcon,
|
||||||
UserRoundSearchIcon,
|
UserRoundSearchIcon,
|
||||||
|
UsersIcon,
|
||||||
|
WebhookIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +52,7 @@ import {
|
|||||||
Blob9,
|
Blob9,
|
||||||
} from './blob';
|
} from './blob';
|
||||||
import { Heading2, Lead2 } from './copy';
|
import { Heading2, Lead2 } from './copy';
|
||||||
|
import { Widget } from './widget';
|
||||||
|
|
||||||
interface SectionItem {
|
interface SectionItem {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -32,7 +60,8 @@ interface SectionItem {
|
|||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
color: string;
|
color: string;
|
||||||
soon?: string;
|
soon?: string;
|
||||||
blob: React.ComponentType<LucideProps>;
|
icons: LucideIcon[];
|
||||||
|
className: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sections: SectionItem[] = [
|
const sections: SectionItem[] = [
|
||||||
@@ -42,7 +71,8 @@ const sections: SectionItem[] = [
|
|||||||
'Take control of your data privacy and ownership with our platform, ensuring full transparency and security.',
|
'Take control of your data privacy and ownership with our platform, ensuring full transparency and security.',
|
||||||
icon: KeyIcon,
|
icon: KeyIcon,
|
||||||
color: '#2563EB',
|
color: '#2563EB',
|
||||||
blob: Blob1,
|
icons: [FolderIcon, DatabaseIcon, ShieldPlusIcon, KeyIcon],
|
||||||
|
className: 'bg-blue-light',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cloud or Self-Hosting',
|
title: 'Cloud or Self-Hosting',
|
||||||
@@ -50,7 +80,8 @@ const sections: SectionItem[] = [
|
|||||||
'Choose between the flexibility of cloud-based hosting or the autonomy of self-hosting to tailor your analytics infrastructure to your needs.',
|
'Choose between the flexibility of cloud-based hosting or the autonomy of self-hosting to tailor your analytics infrastructure to your needs.',
|
||||||
icon: CloudIcon,
|
icon: CloudIcon,
|
||||||
color: '#ff7557',
|
color: '#ff7557',
|
||||||
blob: Blob2,
|
icons: [CloudIcon, CheckCircle, ServerIcon, DownloadIcon],
|
||||||
|
className: '', // 'bg-[#ff7557]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Real-Time Events',
|
title: 'Real-Time Events',
|
||||||
@@ -58,15 +89,17 @@ const sections: SectionItem[] = [
|
|||||||
'Stay up-to-date with real-time event tracking, enabling instant insights into user actions as they happen.',
|
'Stay up-to-date with real-time event tracking, enabling instant insights into user actions as they happen.',
|
||||||
icon: ClockIcon,
|
icon: ClockIcon,
|
||||||
color: '#7fe1d8',
|
color: '#7fe1d8',
|
||||||
blob: Blob3,
|
icons: [CloudLightningIcon, ShoppingCartIcon, ArrowUpFromDotIcon],
|
||||||
|
className: '', // bg-[#7fe1d8]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Deep Dive into User Behavior',
|
title: 'Deep Dive into User Behaviors',
|
||||||
description:
|
description:
|
||||||
"Gain profound insights into user behavior with comprehensive analytics tools, allowing you to understand your audience's actions and preferences.",
|
"Gain profound insights into user behavior with comprehensive analytics tools, allowing you to understand your audience's actions and preferences.",
|
||||||
icon: UserRoundSearchIcon,
|
icon: UserRoundSearchIcon,
|
||||||
color: '#f8bc3c',
|
color: '#f8bc3c',
|
||||||
blob: Blob4,
|
icons: [UsersIcon, RouteIcon, BookmarkIcon],
|
||||||
|
className: 'bg-blue-dark', //'bg-[#f8bc3c]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Powerful Report Explorer',
|
title: 'Powerful Report Explorer',
|
||||||
@@ -74,7 +107,8 @@ const sections: SectionItem[] = [
|
|||||||
'Explore and analyze your data effortlessly with our powerful report explorer, simplifying the process of deriving meaningful insights.',
|
'Explore and analyze your data effortlessly with our powerful report explorer, simplifying the process of deriving meaningful insights.',
|
||||||
icon: CompassIcon,
|
icon: CompassIcon,
|
||||||
color: '#b3596e',
|
color: '#b3596e',
|
||||||
blob: Blob5,
|
icons: [ThumbsUpIcon, TrendingUpIcon, PieChartIcon, BarChart2Icon],
|
||||||
|
className: 'bg-[#ff7557]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
soon: 'Coming soon',
|
soon: 'Coming soon',
|
||||||
@@ -83,7 +117,8 @@ const sections: SectionItem[] = [
|
|||||||
'Track user conversion funnels seamlessly, providing valuable insights into user journey optimization.',
|
'Track user conversion funnels seamlessly, providing valuable insights into user journey optimization.',
|
||||||
icon: ConeIcon,
|
icon: ConeIcon,
|
||||||
color: '#72bef4',
|
color: '#72bef4',
|
||||||
blob: Blob6,
|
icons: [ConeIcon, FilterIcon],
|
||||||
|
className: '', //'bg-[#72bef4]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
soon: 'Coming with our native app',
|
soon: 'Coming with our native app',
|
||||||
@@ -92,7 +127,8 @@ const sections: SectionItem[] = [
|
|||||||
'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.',
|
'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.',
|
||||||
icon: BellIcon,
|
icon: BellIcon,
|
||||||
color: '#ffb27a',
|
color: '#ffb27a',
|
||||||
blob: Blob7,
|
icons: [WebhookIcon, BellIcon],
|
||||||
|
className: '', //'bg-[#ffb27a]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cost-Effective Alternative to Mixpanel',
|
title: 'Cost-Effective Alternative to Mixpanel',
|
||||||
@@ -100,7 +136,8 @@ const sections: SectionItem[] = [
|
|||||||
'Enjoy the same powerful analytics capabilities as Mixpanel at a fraction of the cost, ensuring affordability without compromising on quality.',
|
'Enjoy the same powerful analytics capabilities as Mixpanel at a fraction of the cost, ensuring affordability without compromising on quality.',
|
||||||
icon: DollarSignIcon,
|
icon: DollarSignIcon,
|
||||||
color: '#0f7ea0',
|
color: '#0f7ea0',
|
||||||
blob: Blob8,
|
icons: [DollarSignIcon, HandCoinsIcon, HandshakeIcon, StarIcon],
|
||||||
|
className: 'bg-[#3ba974]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
soon: 'Something Plausible lacks',
|
soon: 'Something Plausible lacks',
|
||||||
@@ -113,61 +150,59 @@ const sections: SectionItem[] = [
|
|||||||
);
|
);
|
||||||
}) as unknown as LucideIcon,
|
}) as unknown as LucideIcon,
|
||||||
color: '#3ba974',
|
color: '#3ba974',
|
||||||
blob: Blob9,
|
icons: [FolderIcon, DatabaseIcon, ShieldPlusIcon, KeyIcon],
|
||||||
|
className: 'bg-[#f8bc3c]',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface SectionProps extends SectionItem {
|
// To lazy to think now...
|
||||||
reverse?: boolean;
|
function checkIndex(index: number) {
|
||||||
}
|
switch (index) {
|
||||||
export function Section({
|
case 0:
|
||||||
title,
|
case 3:
|
||||||
description,
|
case 4:
|
||||||
icon: Icon,
|
case 7:
|
||||||
blob: Blob,
|
case 8:
|
||||||
color,
|
case 10:
|
||||||
soon,
|
return true;
|
||||||
reverse,
|
default:
|
||||||
}: SectionProps) {
|
return false;
|
||||||
return (
|
}
|
||||||
<div key={title} className={'border-b border-border'}>
|
|
||||||
<div className="w-full max-w-6xl mx-auto px-4">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex py-16 flex-col justify-center',
|
|
||||||
reverse ? 'md:flex-row' : 'md:flex-row-reverse'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="md:w-1/2 flex-shrink-0 justify-center items-center flex max-md:mb-8 overflow-hidden rounded-lg">
|
|
||||||
<div className="bg-slate-50 rounded-3xl">
|
|
||||||
<Blob
|
|
||||||
style={{ fill: color }}
|
|
||||||
className="w-[600px] opacity-20 transition-transform animate-[spin_60s_ease-in-out_infinite] -m-[100px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Icon className="w-40 h-40 absolute" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<div className="justify-center flex-col flex">
|
|
||||||
{!!soon && (
|
|
||||||
<div className="rounded-full border border-border p-2 px-4 leading-none mb-4 self-start">
|
|
||||||
{soon}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Heading2 className="mb-4">{title}</Heading2>
|
|
||||||
<Lead2>{description}</Lead2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sections() {
|
export function Sections() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{sections.map((section, index) => (
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 gap-y-16">
|
||||||
<Section key={index} {...section} reverse={index % 2 === 1} />
|
{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 [&_h3]:text-white 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}
|
||||||
|
>
|
||||||
|
<p>{section.description}</p>
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
43
apps/public/src/app/widget.tsx
Normal file
43
apps/public/src/app/widget.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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-105 transition-all duration-300 ease-in-out bg-white hover:shadow min-h-[300px] max-md:col-span-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Heading3 className="mb-4">{title}</Heading3>
|
||||||
|
{children}
|
||||||
|
<div className="flex justify-between mt-auto">
|
||||||
|
{icons.map((Icon, i) => (
|
||||||
|
<Icon
|
||||||
|
key={i}
|
||||||
|
size={120}
|
||||||
|
className={cn('flex-shrink-0 opacity-10 relative', offsets?.[i])}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,43 +1,41 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import useEmblaCarousel, {
|
import { Button } from '@/components/ui/button';
|
||||||
type UseEmblaCarouselType,
|
import { cn } from '@/utils/cn';
|
||||||
} from "embla-carousel-react"
|
import useEmblaCarousel from 'embla-carousel-react';
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
import type { UseEmblaCarouselType } from 'embla-carousel-react';
|
||||||
|
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from "@/utils/cn"
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
import { Button } from "@/components/ui/button"
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
type CarouselApi = UseEmblaCarouselType[1]
|
interface CarouselProps {
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
opts?: CarouselOptions;
|
||||||
type CarouselOptions = UseCarouselParameters[0]
|
plugins?: CarouselPlugin;
|
||||||
type CarouselPlugin = UseCarouselParameters[1]
|
orientation?: 'horizontal' | 'vertical';
|
||||||
|
setApi?: (api: CarouselApi) => void;
|
||||||
type CarouselProps = {
|
|
||||||
opts?: CarouselOptions
|
|
||||||
plugins?: CarouselPlugin
|
|
||||||
orientation?: "horizontal" | "vertical"
|
|
||||||
setApi?: (api: CarouselApi) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CarouselContextProps = {
|
type CarouselContextProps = {
|
||||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
scrollPrev: () => void
|
scrollPrev: () => void;
|
||||||
scrollNext: () => void
|
scrollNext: () => void;
|
||||||
canScrollPrev: boolean
|
canScrollPrev: boolean;
|
||||||
canScrollNext: boolean
|
canScrollNext: boolean;
|
||||||
} & CarouselProps
|
} & CarouselProps;
|
||||||
|
|
||||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
function useCarousel() {
|
function useCarousel() {
|
||||||
const context = React.useContext(CarouselContext)
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useCarousel must be used within a <Carousel />")
|
throw new Error('useCarousel must be used within a <Carousel />');
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Carousel = React.forwardRef<
|
const Carousel = React.forwardRef<
|
||||||
@@ -46,7 +44,7 @@ const Carousel = React.forwardRef<
|
|||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
orientation = "horizontal",
|
orientation = 'horizontal',
|
||||||
opts,
|
opts,
|
||||||
setApi,
|
setApi,
|
||||||
plugins,
|
plugins,
|
||||||
@@ -59,64 +57,64 @@ const Carousel = React.forwardRef<
|
|||||||
const [carouselRef, api] = useEmblaCarousel(
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
{
|
{
|
||||||
...opts,
|
...opts,
|
||||||
axis: orientation === "horizontal" ? "x" : "y",
|
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||||
},
|
},
|
||||||
plugins
|
plugins
|
||||||
)
|
);
|
||||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
if (!api) {
|
if (!api) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCanScrollPrev(api.canScrollPrev())
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
setCanScrollNext(api.canScrollNext())
|
setCanScrollNext(api.canScrollNext());
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const scrollPrev = React.useCallback(() => {
|
const scrollPrev = React.useCallback(() => {
|
||||||
api?.scrollPrev()
|
api?.scrollPrev();
|
||||||
}, [api])
|
}, [api]);
|
||||||
|
|
||||||
const scrollNext = React.useCallback(() => {
|
const scrollNext = React.useCallback(() => {
|
||||||
api?.scrollNext()
|
api?.scrollNext();
|
||||||
}, [api])
|
}, [api]);
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
const handleKeyDown = React.useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key === "ArrowLeft") {
|
if (event.key === 'ArrowLeft') {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
scrollPrev()
|
scrollPrev();
|
||||||
} else if (event.key === "ArrowRight") {
|
} else if (event.key === 'ArrowRight') {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
scrollNext()
|
scrollNext();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[scrollPrev, scrollNext]
|
[scrollPrev, scrollNext]
|
||||||
)
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api || !setApi) {
|
if (!api || !setApi) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setApi(api)
|
setApi(api);
|
||||||
}, [api, setApi])
|
}, [api, setApi]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api) {
|
if (!api) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelect(api)
|
onSelect(api);
|
||||||
api.on("reInit", onSelect)
|
api.on('reInit', onSelect);
|
||||||
api.on("select", onSelect)
|
api.on('select', onSelect);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
api?.off("select", onSelect)
|
api?.off('select', onSelect);
|
||||||
}
|
};
|
||||||
}, [api, onSelect])
|
}, [api, onSelect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CarouselContext.Provider
|
<CarouselContext.Provider
|
||||||
@@ -125,7 +123,7 @@ const Carousel = React.forwardRef<
|
|||||||
api: api,
|
api: api,
|
||||||
opts,
|
opts,
|
||||||
orientation:
|
orientation:
|
||||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||||
scrollPrev,
|
scrollPrev,
|
||||||
scrollNext,
|
scrollNext,
|
||||||
canScrollPrev,
|
canScrollPrev,
|
||||||
@@ -135,7 +133,7 @@ const Carousel = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
className={cn("relative", className)}
|
className={cn('relative', className)}
|
||||||
role="region"
|
role="region"
|
||||||
aria-roledescription="carousel"
|
aria-roledescription="carousel"
|
||||||
{...props}
|
{...props}
|
||||||
@@ -143,38 +141,38 @@ const Carousel = React.forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</CarouselContext.Provider>
|
</CarouselContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Carousel.displayName = "Carousel"
|
Carousel.displayName = 'Carousel';
|
||||||
|
|
||||||
const CarouselContent = React.forwardRef<
|
const CarouselContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { carouselRef, orientation } = useCarousel()
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={carouselRef} className="overflow-hidden">
|
<div ref={carouselRef} className="overflow-hidden">
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex",
|
'flex',
|
||||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
CarouselContent.displayName = "CarouselContent"
|
CarouselContent.displayName = 'CarouselContent';
|
||||||
|
|
||||||
const CarouselItem = React.forwardRef<
|
const CarouselItem = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { orientation } = useCarousel()
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -182,21 +180,21 @@ const CarouselItem = React.forwardRef<
|
|||||||
role="group"
|
role="group"
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 shrink-0 grow-0 basis-full",
|
'min-w-0 shrink-0 grow-0 basis-full',
|
||||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
CarouselItem.displayName = "CarouselItem"
|
CarouselItem.displayName = 'CarouselItem';
|
||||||
|
|
||||||
const CarouselPrevious = React.forwardRef<
|
const CarouselPrevious = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<typeof Button>
|
React.ComponentProps<typeof Button>
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -204,10 +202,10 @@ const CarouselPrevious = React.forwardRef<
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-8 w-8 rounded-full",
|
'absolute h-8 w-8 rounded-full',
|
||||||
orientation === "horizontal"
|
orientation === 'horizontal'
|
||||||
? "-left-12 top-1/2 -translate-y-1/2"
|
? '-left-12 top-1/2 -translate-y-1/2'
|
||||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollPrev}
|
disabled={!canScrollPrev}
|
||||||
@@ -217,15 +215,15 @@ const CarouselPrevious = React.forwardRef<
|
|||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
CarouselPrevious.displayName = "CarouselPrevious"
|
CarouselPrevious.displayName = 'CarouselPrevious';
|
||||||
|
|
||||||
const CarouselNext = React.forwardRef<
|
const CarouselNext = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<typeof Button>
|
React.ComponentProps<typeof Button>
|
||||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -233,10 +231,10 @@ const CarouselNext = React.forwardRef<
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-8 w-8 rounded-full",
|
'absolute h-8 w-8 rounded-full',
|
||||||
orientation === "horizontal"
|
orientation === 'horizontal'
|
||||||
? "-right-12 top-1/2 -translate-y-1/2"
|
? '-right-12 top-1/2 -translate-y-1/2'
|
||||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollNext}
|
disabled={!canScrollNext}
|
||||||
@@ -246,9 +244,9 @@ const CarouselNext = React.forwardRef<
|
|||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
CarouselNext.displayName = "CarouselNext"
|
CarouselNext.displayName = 'CarouselNext';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type CarouselApi,
|
type CarouselApi,
|
||||||
@@ -257,4 +255,4 @@ export {
|
|||||||
CarouselItem,
|
CarouselItem,
|
||||||
CarouselPrevious,
|
CarouselPrevious,
|
||||||
CarouselNext,
|
CarouselNext,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fancy-text {
|
.fancy-text {
|
||||||
@apply text-transparent inline-block bg-gradient-to-br from-blue-600 to-purple-900 bg-clip-text;
|
@apply text-transparent inline-block bg-gradient-to-br from-blue-200 to-blue-400 bg-clip-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
|
|||||||
@@ -1,22 +1,5 @@
|
|||||||
const colors = [
|
|
||||||
'#2563EB',
|
|
||||||
'#ff7557',
|
|
||||||
'#7fe1d8',
|
|
||||||
'#f8bc3c',
|
|
||||||
'#b3596e',
|
|
||||||
'#72bef4',
|
|
||||||
'#ffb27a',
|
|
||||||
'#0f7ea0',
|
|
||||||
'#3ba974',
|
|
||||||
'#febbb2',
|
|
||||||
'#cb80dc',
|
|
||||||
'#5cb7af',
|
|
||||||
'#7856ff',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
safelist: [...colors.map((color) => `chart-${color}`)],
|
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{ts,tsx}',
|
'./pages/**/*.{ts,tsx}',
|
||||||
'./components/**/*.{ts,tsx}',
|
'./components/**/*.{ts,tsx}',
|
||||||
@@ -24,8 +7,16 @@ const config = {
|
|||||||
'./src/**/*.{ts,tsx}',
|
'./src/**/*.{ts,tsx}',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '1rem',
|
||||||
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
['blue-light']: '#2C97FE',
|
||||||
|
['blue-dark']: '#111F46',
|
||||||
|
['blue-darker']: '#051030',
|
||||||
|
|
||||||
border: 'hsl(var(--border))',
|
border: 'hsl(var(--border))',
|
||||||
input: 'hsl(var(--input))',
|
input: 'hsl(var(--input))',
|
||||||
ring: 'hsl(var(--ring))',
|
ring: 'hsl(var(--ring))',
|
||||||
@@ -59,12 +50,6 @@ const config = {
|
|||||||
DEFAULT: 'hsl(var(--card))',
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: 'hsl(var(--card-foreground))',
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
},
|
},
|
||||||
...colors.reduce((acc, color, index) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[`chart-${index}`]: color,
|
|
||||||
};
|
|
||||||
}, {}),
|
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: 'var(--radius)',
|
lg: 'var(--radius)',
|
||||||
|
|||||||
Reference in New Issue
Block a user