public: new page and copy improvements
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
113
apps/public/src/components/illustrations/data-ownership.tsx
Normal file
113
apps/public/src/components/illustrations/data-ownership.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
99
apps/public/src/components/illustrations/privacy.tsx
Normal file
99
apps/public/src/components/illustrations/privacy.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
101
apps/public/src/components/illustrations/product-analytics.tsx
Normal file
101
apps/public/src/components/illustrations/product-analytics.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
189
apps/public/src/components/illustrations/web-analytics.tsx
Normal file
189
apps/public/src/components/illustrations/web-analytics.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -21,10 +21,10 @@ const LINKS = [
|
||||
text: 'Pricing',
|
||||
url: '/pricing',
|
||||
},
|
||||
{
|
||||
text: 'Supporter',
|
||||
url: '/supporter',
|
||||
},
|
||||
// {
|
||||
// text: 'Supporter',
|
||||
// url: '/supporter',
|
||||
// },
|
||||
{
|
||||
text: 'Docs',
|
||||
url: '/docs',
|
||||
|
||||
22
apps/public/src/components/testimonial.tsx
Normal file
22
apps/public/src/components/testimonial.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user