public: new page and copy improvements

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-02-17 00:16:08 +01:00
parent 0ebe2768be
commit e3faab7588
22 changed files with 1026 additions and 352 deletions

View File

@@ -1,6 +1,6 @@
import { cn } from '@/lib/utils';
import type { LucideIcon } from 'lucide-react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
interface FeatureCardProps {
link?: {
@@ -21,12 +21,17 @@ interface FeatureCardContainerProps {
className?: string;
}
export const FeatureCardBackground = () => (
export const FeatureCardBackground = ({
interactive = true,
}: {
interactive?: boolean;
}) => (
<div
className={cn(
'pointer-events-none absolute inset-0 bg-linear-to-br opacity-0 blur-2xl transition-opacity duration-300 group-hover:opacity-100',
'dark:from-blue-500/10 dark:via-transparent dark:to-emerald-500/5',
'dark:from-blue-500/20 dark:via-transparent dark:to-emerald-500/10',
'light:from-blue-800/20 light:via-transparent light:to-emerald-900/10',
interactive === false && 'opacity-100'
)}
/>
);
@@ -38,8 +43,8 @@ export function FeatureCardContainer({
return (
<div
className={cn(
'col gap-8 p-6 rounded-3xl border bg-background group relative overflow-hidden',
className,
'col group relative gap-8 overflow-hidden rounded-3xl border bg-background p-6',
className
)}
>
<FeatureCardBackground />
@@ -62,13 +67,13 @@ export function FeatureCard({
<FeatureCardContainer className={className}>
{illustration}
<div className="col gap-2" data-content>
<h3 className="text-xl font-semibold">{title}</h3>
<h3 className="font-semibold text-xl">{title}</h3>
<p className="text-muted-foreground">{description}</p>
</div>
{children}
{link && (
<Link
className="mx-6 text-sm text-muted-foreground hover:text-primary transition-colors"
className="mx-6 text-muted-foreground text-sm transition-colors hover:text-primary"
href={link.href}
>
{link.children}
@@ -82,13 +87,13 @@ export function FeatureCard({
<FeatureCardContainer className={className}>
{Icon && <Icon className="size-6" />}
<div className="col gap-2">
<h3 className="text-lg font-semibold">{title}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
<h3 className="font-semibold text-lg">{title}</h3>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
{children}
{link && (
<Link
className="text-sm text-muted-foreground hover:text-primary transition-colors"
className="text-muted-foreground text-sm transition-colors hover:text-primary"
href={link.href}
>
{link.children}

View File

@@ -1,22 +1,22 @@
import { TOOLS } from '@/app/tools/tools';
import { articleSource, compareSource, featureSource } from '@/lib/source';
import { MailIcon } from 'lucide-react';
import Link from 'next/link';
import { Logo } from './logo';
import { TOOLS } from '@/app/tools/tools';
import { articleSource, compareSource, featureSource } from '@/lib/source';
export async function Footer() {
const articles = (await articleSource.getPages()).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
const year = new Date().getFullYear();
return (
<div>
<footer className="text-sm relative overflow-hidden pt-32">
<div className="absolute -bottom-20 md:-bottom-32 left-0 right-0 center-center opacity-5 pointer-events-none">
<footer className="relative overflow-hidden pt-32 text-sm">
<div className="center-center pointer-events-none absolute right-0 -bottom-20 left-0 opacity-5 md:-bottom-32">
<div className="absolute inset-0 bg-linear-to-b from-background to-transparent" />
<Logo className="w-[900px] shrink-0" />
</div>
<div className="container grid grid-cols-1 md:grid-cols-4 gap-12 md:gap-8 relative">
<div className="container relative grid grid-cols-1 gap-12 md:grid-cols-4 md:gap-8">
<div className="col gap-3">
<h3 className="font-medium">Useful links</h3>
<Links
@@ -28,6 +28,10 @@ export async function Footer() {
title: 'Free analytics for open source projects',
url: '/open-source',
},
{
title: 'Open source analytics',
url: '/open-source-analytics',
},
]}
/>
<div className="h-5" />
@@ -86,8 +90,8 @@ export async function Footer() {
</div>
</div>
<div className="col text-muted-foreground border-t pt-8 mt-16 gap-8 relative bg-background/70 pb-32">
<div className="container col md:row justify-between gap-8">
<div className="col relative mt-16 gap-8 border-t bg-background/70 pt-8 pb-32 text-muted-foreground">
<div className="col md:row container justify-between gap-8">
<div>
<a
href="https://openpanel.dev"
@@ -100,21 +104,21 @@ export async function Footer() {
}}
>
<iframe
src="https://dashboard.openpanel.dev/widget/badge?shareId=ancygl&color=%230B0B0B"
height="48"
width="100%"
src="https://dashboard.openpanel.dev/widget/badge?shareId=ancygl&color=%230B0B0B"
style={{
border: 'none',
overflow: 'hidden',
pointerEvents: 'none',
}}
title="OpenPanel Analytics Badge"
width="100%"
/>
</a>
</div>
<Social />
</div>
<div className="container flex flex-col-reverse md:row justify-between gap-8">
<div className="md:row container flex flex-col-reverse justify-between gap-8">
<div>Copyright © {year} OpenPanel. All rights reserved.</div>
<div className="col lg:row gap-2 md:gap-4">
<Link href="/sitemap.xml">Sitemap</Link>
@@ -131,12 +135,12 @@ export async function Footer() {
function Links({ data }: { data: { title: string; url: string }[] }) {
return (
<ul className="gap-2 col text-muted-foreground">
<ul className="col gap-2 text-muted-foreground">
{data.map((item) => (
<li key={item.url} className="truncate">
<li className="truncate" key={item.url}>
<Link
className="transition-colors hover:text-foreground"
href={item.url}
className="hover:text-foreground transition-colors"
title={item.title}
>
{item.title}
@@ -149,68 +153,68 @@ function Links({ data }: { data: { title: string; url: string }[] }) {
function Social() {
return (
<div className="md:items-end col gap-4">
<div className="[&_svg]:size-6 row gap-4">
<div className="col gap-4 md:items-end">
<div className="row gap-4 [&_svg]:size-6">
<Link
title="Go to GitHub"
href="https://github.com/Openpanel-dev/openpanel"
rel="noreferrer noopener nofollow"
title="Go to GitHub"
>
<svg
className="fill-current"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="fill-current"
>
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
</Link>
<Link
title="Go to X"
href="https://x.com/openpaneldev"
rel="noreferrer noopener nofollow"
title="Go to X"
>
<svg
className="fill-current"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="fill-current"
>
<title>X</title>
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H3.298Z" />
</svg>
</Link>
<Link
title="Join Discord"
href="https://go.openpanel.dev/discord"
rel="noreferrer noopener nofollow"
title="Join Discord"
>
<svg
className="fill-current"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="fill-current"
>
<title>Discord</title>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
</Link>
<Link
title="Send an email"
href="mailto:hello@openpanel.dev"
rel="noreferrer noopener nofollow"
title="Send an email"
>
<MailIcon className="size-6" />
</Link>
<a
target="_blank"
className="row items-center gap-2 rounded-full border px-2 py-1 max-md:ml-auto max-md:self-start"
href="https://status.openpanel.dev"
className="row gap-2 items-center border rounded-full px-2 py-1 max-md:self-start max-md:ml-auto"
rel="noreferrer noopener nofollow"
target="_blank"
>
<span>Operational</span>
<div className="size-2 bg-emerald-500 rounded-full" />
<div className="size-2 rounded-full bg-emerald-500" />
</a>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { cn } from '@/lib/utils';
import { ChevronRightIcon } from 'lucide-react';
import Link from 'next/link';
import { Button } from './ui/button';
import { cn } from '@/lib/utils';
export function GetStartedButton({
text,
@@ -13,10 +13,10 @@ export function GetStartedButton({
href?: string;
}) {
return (
<Button size="lg" asChild className={cn('group', className)}>
<Button asChild className={cn('group', className)} size="lg">
<Link href={href}>
{text ?? 'Get started now'}
<ChevronRightIcon className="size-4 group-hover:translate-x-1 transition-transform group-hover:scale-125" />
{text ?? 'Start free trial'}
<ChevronRightIcon className="size-4 transition-transform group-hover:translate-x-1 group-hover:scale-125" />
</Link>
</Button>
);

View File

@@ -0,0 +1,113 @@
import React from 'react';
type IllustrationProps = {
className?: string;
};
export function DataOwnershipIllustration({
className = '',
}: IllustrationProps) {
return (
<div>
{/* Main layout */}
<div className="relative grid aspect-2/1 grid-cols-5 gap-3">
{/* Left: your server card */}
<div
className="
col-span-3 rounded-2xl border border-border bg-card/80
p-3 sm:p-4 shadow-xl backdrop-blur
transition-all duration-300
group-hover:-translate-y-1 group-hover:-translate-x-0.5
"
>
<div className="flex items-center justify-between text-xs text-foreground">
<span>Your server</span>
<span className="flex items-center gap-1 rounded-full bg-card/80 px-2 py-0.5 text-[10px] text-blue-300">
<span className="h-1.5 w-1.5 rounded-full bg-blue-400" />
In control
</span>
</div>
{/* "Server" visual */}
<div className="mt-3 space-y-2">
<div className="flex gap-1.5">
<div className="flex-1 rounded-xl bg-card/80 border border-border px-3 py-2">
<p className="text-[10px] text-muted-foreground">Region</p>
<p className="text-xs font-medium text-foreground">
EU / Custom
</p>
</div>
<div className="flex-1 rounded-xl bg-card/80 border border-border px-3 py-2">
<p className="text-[10px] text-muted-foreground">Retention</p>
<p className="text-xs font-medium text-foreground">
Configurable
</p>
</div>
</div>
{/* mini "database"/requests strip */}
<div className="mt-1 rounded-xl border border-border bg-card/90 px-3 py-2 text-[11px] text-foreground">
<div className="flex items-center justify-between">
<span>Events stored</span>
<span className="text-[10px] text-muted-foreground">
locally
</span>
</div>
<div className="mt-2 flex gap-1.5">
<div className="h-1.5 flex-1 rounded-full bg-blue-400/70" />
<div className="h-1.5 flex-1 rounded-full bg-blue-400/40" />
<div className="h-1.5 flex-1 rounded-full bg-blue-400/20" />
</div>
</div>
<div className="mt-1 rounded-xl border border-border bg-card/90 px-3 py-2 text-[11px] text-foreground">
<div className="flex items-center justify-between">
<span>CPU</span>
<span className="text-[10px] text-muted-foreground">20%</span>
</div>
<div className="mt-2 flex gap-1.5">
<div className="h-1.5 flex-1 rounded-full bg-blue-400/70" />
<div className="h-1.5 flex-1 rounded-full bg-blue-400/40" />
<div className="h-1.5 flex-1 rounded-full bg-blue-400/20" />
</div>
</div>
</div>
</div>
{/* Right: third-party contrast */}
<div
className="
col-span-2 rounded-2xl border border-border/80 bg-card/40
p-3 text-[11px] text-muted-foreground
transition-all duration-300
group-hover:translate-y-1 group-hover:translate-x-0.5 group-hover:opacity-70
"
>
<p className="text-xs text-muted-foreground mb-2">or use our cloud</p>
<ul className="space-y-1.5">
<li className="flex items-center gap-1.5">
<span className="h-1 w-1 rounded-full bg-blue-400" />
Zero server setup
</li>
<li className="flex items-center gap-1.5">
<span className="h-1 w-1 rounded-full bg-blue-400" />
Auto-scaling & backups
</li>
<li className="flex items-center gap-1.5">
<span className="h-1 w-1 rounded-full bg-blue-400" />
99.9% uptime
</li>
<li className="flex items-center gap-1.5">
<span className="h-1 w-1 rounded-full bg-blue-400" />
24/7 support
</li>
<li className="flex items-center gap-1.5">
<span className="h-1 w-1 rounded-full bg-blue-400" />
Export data anytime
</li>
</ul>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,99 @@
import React from 'react';
type IllustrationProps = {
className?: string;
};
export function PrivacyIllustration({ className = '' }: IllustrationProps) {
return (
<div>
{/* Floating cards */}
<div className="relative aspect-3/2 md:aspect-2/1">
{/* Back card */}
<div
className="
absolute top-0 left-0 right-10 bottom-10 rounded-2xl border border-border/80 bg-card/70
backdrop-blur-sm shadow-lg
transition-all duration-300
group-hover:-translate-y-1 group-hover:-rotate-2
"
>
<div className="flex items-center justify-between px-4 pt-3 text-xs text-muted-foreground">
<span>Session duration</span>
<span className="flex items-center gap-1">
3m 12s
<span className="text-[10px] text-blue-400">+8%</span>
</span>
</div>
{/* Simple line chart */}
<div className="mt-3 px-4">
<svg
viewBox="0 0 120 40"
className="h-16 w-full text-muted-foreground"
>
<path
d="M2 32 L22 18 L40 24 L60 10 L78 16 L96 8 L118 14"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
className="opacity-60"
/>
<circle cx="118" cy="14" r="2.5" className="fill-blue-400" />
</svg>
</div>
</div>
{/* Front card */}
<div
className="
col
absolute top-10 left-4 right-0 bottom-0 rounded-2xl border border-border/80
bg-card shadow-xl
transition-all duration-300
group-hover:translate-y-1 group-hover:rotate-2
"
>
<div className="flex items-center justify-between px-4 pt-3 text-xs text-foreground">
<span>Anonymous visitors</span>
<span className="text-[10px] rounded-full bg-card px-2 py-0.5 text-muted-foreground">
No cookies
</span>
</div>
<div className="flex items-end justify-between px-4 pt-4 pb-3">
<div>
<p className="text-[11px] text-muted-foreground mb-1">
Active now
</p>
<p className="text-2xl font-semibold text-foreground">128</p>
</div>
<div className="space-y-1.5 text-[10px] text-muted-foreground">
<div className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-blue-400" />
<span>IP + UA hashed daily</span>
</div>
<div className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-blue-400" />
<span>No fingerprinting</span>
</div>
</div>
</div>
{/* "Sources" row */}
<div className="mt-auto flex gap-2 border-t border-border px-3 py-2.5 text-[11px]">
<div className="flex-1 rounded-xl bg-card/90 px-3 py-1.5 flex items-center justify-between">
<span className="text-muted-foreground">Direct</span>
<span className="text-foreground">42%</span>
</div>
<div className="flex-1 rounded-xl bg-card/90 px-3 py-1.5 flex items-center justify-between">
<span className="text-muted-foreground">Organic</span>
<span className="text-foreground">58%</span>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,101 @@
'use client';
import { cn } from '@/lib/utils';
import { ResponsiveFunnel } from '@nivo/funnel';
import NumberFlow from '@number-flow/react';
import { AnimatePresence, motion, useSpring } from 'framer-motion';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
function useFunnelSteps() {
const { resolvedTheme } = useTheme();
return [
{
id: 'Visitors',
label: 'Visitors',
value: 10000,
percentage: 100,
color: resolvedTheme === 'dark' ? '#333' : '#888',
},
{
id: 'Add to cart',
label: 'Add to cart',
value: 7000,
percentage: 32,
color: resolvedTheme === 'dark' ? '#222' : '#999',
},
{
id: 'Checkout',
label: 'Checkout',
value: 5000,
percentage: 8.9,
color: resolvedTheme === 'dark' ? '#111' : '#e1e1e1',
},
];
}
export function ProductAnalyticsIllustration() {
return (
<div className="aspect-video">
<FunnelVisualization />
</div>
);
}
export const PartLabel = ({ part }: { part: any }) => {
const { resolvedTheme } = useTheme();
return (
<g transform={`translate(${part.x}, ${part.y})`}>
<text
textAnchor="middle"
dominantBaseline="central"
style={{
fill: resolvedTheme === 'dark' ? '#fff' : '#000',
pointerEvents: 'none',
fontSize: 12,
fontWeight: 500,
}}
>
{part.data.label}
</text>
</g>
);
};
function Labels(props: any) {
return props.parts.map((part: any) => (
<PartLabel key={part.data.id} part={part} />
));
}
function FunnelVisualization() {
const funnelSteps = useFunnelSteps();
const colors = funnelSteps.map((stage) => stage.color);
const nivoData = funnelSteps.map((stage) => ({
id: stage.id,
value: stage.value,
label: stage.label,
}));
return (
<div className="w-full h-full">
<ResponsiveFunnel
data={nivoData}
margin={{ top: 20, right: 0, bottom: 20, left: 0 }}
direction="horizontal"
shapeBlending={0.6}
colors={colors}
enableBeforeSeparators={false}
enableAfterSeparators={false}
beforeSeparatorLength={0}
afterSeparatorLength={0}
afterSeparatorOffset={0}
beforeSeparatorOffset={0}
currentPartSizeExtension={5}
borderWidth={20}
currentBorderWidth={15}
tooltip={() => null}
layers={['parts', Labels]}
/>
</div>
);
}

View File

@@ -0,0 +1,189 @@
'use client';
import { SimpleChart } from '@/components/simple-chart';
import { cn } from '@/lib/utils';
import NumberFlow from '@number-flow/react';
import { AnimatePresence, motion } from 'framer-motion';
import { ArrowUpIcon } from 'lucide-react';
import Image from 'next/image';
import { useEffect, useState } from 'react';
const TRAFFIC_SOURCES = [
{
icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Fgoogle.com',
name: 'Google',
percentage: 49,
value: 2039,
},
{
icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Finstagram.com',
name: 'Instagram',
percentage: 23,
value: 920,
},
{
icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Ffacebook.com',
name: 'Facebook',
percentage: 18,
value: 750,
},
{
icon: 'https://api.openpanel.dev/misc/favicon?url=https%3A%2F%2Ftwitter.com',
name: 'Twitter',
percentage: 10,
value: 412,
},
];
const COUNTRIES = [
{ icon: '🇺🇸', name: 'United States', percentage: 37, value: 1842 },
{ icon: '🇩🇪', name: 'Germany', percentage: 28, value: 1391 },
{ icon: '🇬🇧', name: 'United Kingdom', percentage: 20, value: 982 },
{ icon: '🇯🇵', name: 'Japan', percentage: 15, value: 751 },
];
export function WebAnalyticsIllustration() {
const [currentSourceIndex, setCurrentSourceIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrentSourceIndex((prev) => (prev + 1) % TRAFFIC_SOURCES.length);
}, 3000);
return () => clearInterval(interval);
}, []);
return (
<div className="px-12 group aspect-video">
<div className="relative h-full col">
<MetricCard
title="Session duration"
value="3m 23s"
change="3%"
chartPoints={[40, 10, 20, 43, 20, 40, 30, 37, 40, 34, 50, 31]}
color="var(--foreground)"
className="absolute w-full rotate-0 top-2 left-2 group-hover:-translate-y-1 group-hover:-rotate-2 transition-all duration-300"
/>
<MetricCard
title="Bounce rate"
value="46%"
change="3%"
chartPoints={[10, 46, 20, 43, 20, 40, 30, 37, 40, 34, 50, 31]}
color="var(--foreground)"
className="absolute w-full -rotate-2 -left-2 top-12 group-hover:-translate-y-1 group-hover:rotate-0 transition-all duration-300"
/>
<div className="col gap-4 w-[80%] md:w-[70%] ml-auto mt-auto">
<BarCell
{...TRAFFIC_SOURCES[currentSourceIndex]}
className="group-hover:scale-105 transition-all duration-300"
/>
<BarCell
{...TRAFFIC_SOURCES[
(currentSourceIndex + 1) % TRAFFIC_SOURCES.length
]}
className="group-hover:scale-105 transition-all duration-300"
/>
</div>
</div>
</div>
);
}
function MetricCard({
title,
value,
change,
chartPoints,
color,
className,
}: {
title: string;
value: string;
change: string;
chartPoints: number[];
color: string;
className?: string;
}) {
return (
<div className={cn('col bg-card rounded-lg p-4 pb-6 border', className)}>
<div className="row items-end justify-between">
<div>
<div className="text-muted-foreground text-sm">{title}</div>
<div className="text-2xl font-semibold font-mono">{value}</div>
</div>
<div className="row gap-2 items-center font-mono font-medium">
<ArrowUpIcon className="size-3" strokeWidth={3} />
<div>{change}</div>
</div>
</div>
<SimpleChart
width={400}
height={30}
points={chartPoints}
strokeColor={color}
className="mt-4"
/>
</div>
);
}
function BarCell({
icon,
name,
percentage,
value,
className,
}: {
icon: string;
name: string;
percentage: number;
value: number;
className?: string;
}) {
return (
<div
className={cn(
'relative p-4 py-2 bg-card rounded-lg shadow-[0_10px_30px_rgba(0,0,0,0.3)] border',
className,
)}
>
<div
className="absolute bg-background bottom-0 top-0 left-0 rounded-lg transition-all duration-500"
style={{
width: `${percentage}%`,
}}
/>
<div className="relative row justify-between ">
<div className="row gap-2 items-center font-medium text-sm">
{icon.startsWith('http') ? (
<Image
alt="serie icon"
className="max-h-4 rounded-[2px] object-contain"
src={icon}
width={16}
height={16}
/>
) : (
<div className="text-2xl">{icon}</div>
)}
<AnimatePresence mode="popLayout">
<motion.div
key={name}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.3 }}
>
{name}
</motion.div>
</AnimatePresence>
</div>
<div className="row gap-3 font-mono text-sm">
<span className="text-muted-foreground">
<NumberFlow value={percentage} />%
</span>
<NumberFlow value={value} locales={'en-US'} />
</div>
</div>
</div>
);
}

View File

@@ -21,10 +21,10 @@ const LINKS = [
text: 'Pricing',
url: '/pricing',
},
{
text: 'Supporter',
url: '/supporter',
},
// {
// text: 'Supporter',
// url: '/supporter',
// },
{
text: 'Docs',
url: '/docs',

View File

@@ -0,0 +1,22 @@
import { QuoteIcon } from 'lucide-react';
import { FeatureCardBackground } from '@/components/feature-card';
import { cn } from '@/lib/utils';
interface TestimonialProps {
quote: string;
author: string;
className?: string;
}
export function Testimonial({ quote, author, className }: TestimonialProps) {
return (
<figure className={cn('group relative', className)}>
<FeatureCardBackground interactive={false} />
<QuoteIcon className="group-hover:-translate-1 mb-2 size-8 stroke-1 text-muted-foreground/50 transition-all group-hover:-rotate-6 group-hover:scale-105 group-hover:text-foreground" />
<blockquote className="text-2xl">{quote}</blockquote>
<figcaption className="mt-2 text-muted-foreground text-sm">
{author}
</figcaption>
</figure>
);
}