chore:little fixes and formating and linting and patches
This commit is contained in:
@@ -18,23 +18,23 @@ export function ArticleCard({
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
className="col overflow-hidden rounded-lg border bg-background-light transition-all duration-300 hover:scale-105 hover:shadow-background-dark hover:shadow-lg"
|
||||
href={url}
|
||||
key={url}
|
||||
className="border rounded-lg overflow-hidden bg-background-light col hover:scale-105 transition-all duration-300 hover:shadow-lg hover:shadow-background-dark"
|
||||
>
|
||||
<Image
|
||||
src={cover}
|
||||
alt={title}
|
||||
width={323}
|
||||
height={181}
|
||||
className="w-full"
|
||||
height={181}
|
||||
src={cover}
|
||||
width={323}
|
||||
/>
|
||||
<span className="p-4 col flex-1">
|
||||
{tag && <span className="font-mono text-xs mb-2">{tag}</span>}
|
||||
<span className="flex-1 mb-6">
|
||||
<h2 className="text-xl font-semibold">{title}</h2>
|
||||
<span className="col flex-1 p-4">
|
||||
{tag && <span className="mb-2 font-mono text-xs">{tag}</span>}
|
||||
<span className="mb-6 flex-1">
|
||||
<h2 className="font-semibold text-xl">{title}</h2>
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{[team, date.toLocaleDateString()].filter(Boolean).join(' · ')}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
@@ -45,31 +45,31 @@ export function Competition() {
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<span className="block truncate leading-tight -mt-1" style={{ color }}>
|
||||
<span className="-mt-1 block truncate leading-tight" style={{ color }}>
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<motion.div
|
||||
className="-mt-1 block truncate leading-tight"
|
||||
key={word}
|
||||
className="block truncate leading-tight -mt-1"
|
||||
style={{ color }}
|
||||
>
|
||||
{word?.split('').map((char, index) => (
|
||||
<motion.span
|
||||
key={`${word}-${char}-${index.toString()}`}
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: -10, opacity: 0 }}
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
key={`${word}-${char}-${index.toString()}`}
|
||||
style={{ display: 'inline-block', whiteSpace: 'pre' }}
|
||||
transition={{
|
||||
duration: 0.15,
|
||||
delay: index * 0.015,
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
style={{ display: 'inline-block', whiteSpace: 'pre' }}
|
||||
>
|
||||
{char}
|
||||
</motion.span>
|
||||
|
||||
@@ -27,8 +27,8 @@ export function EuFlag({ className }: { className?: string }) {
|
||||
{STARS.map((s, i) => (
|
||||
<polygon
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static data
|
||||
key={i}
|
||||
fill="#FFCC00"
|
||||
key={i}
|
||||
points={star(s.x, s.y, 1.1, 0.45)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { FeatureCardHoverTrack } from '@/components/feature-card-hover-track';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface FeatureCardProps {
|
||||
link?: {
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import Image from 'next/image';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Figure({
|
||||
src,
|
||||
alt,
|
||||
caption,
|
||||
className,
|
||||
}: { src: string; alt: string; caption: string; className?: string }) {
|
||||
}: {
|
||||
src: string;
|
||||
alt: string;
|
||||
caption: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<figure className={cn('-mx-4', className)}>
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt || caption}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="rounded-lg"
|
||||
height={800}
|
||||
src={src}
|
||||
width={1200}
|
||||
/>
|
||||
<figcaption className="text-center text-sm text-muted-foreground mt-2">
|
||||
<figcaption className="mt-2 text-center text-muted-foreground text-sm">
|
||||
{caption}
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
@@ -45,15 +45,15 @@ export function FlowStep({
|
||||
const Icon = iconMap[icon];
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-4 mb-4 min-w-0">
|
||||
<div className="relative mb-4 flex min-w-0 gap-4">
|
||||
{/* Step number and icon */}
|
||||
<div className="flex flex-col items-center shrink-0">
|
||||
<div className="flex shrink-0 flex-col items-center">
|
||||
<div className="relative z-10 bg-background">
|
||||
<div className="flex items-center justify-center size-10 rounded-full bg-primary text-primary-foreground font-semibold text-sm shadow-sm">
|
||||
<div className="flex size-10 items-center justify-center rounded-full bg-primary font-semibold text-primary-foreground text-sm shadow-sm">
|
||||
{step}
|
||||
</div>
|
||||
<div
|
||||
className={`absolute -bottom-2 -right-2 flex items-center justify-center w-6 h-6 rounded-full bg-background border shadow-sm ${iconBorderColorMap[icon] || 'border-primary'}`}
|
||||
className={`absolute -right-2 -bottom-2 flex h-6 w-6 items-center justify-center rounded-full border bg-background shadow-sm ${iconBorderColorMap[icon] || 'border-primary'}`}
|
||||
>
|
||||
<Icon
|
||||
className={`size-3.5 ${iconColorMap[icon] || 'text-primary'}`}
|
||||
@@ -62,14 +62,14 @@ export function FlowStep({
|
||||
</div>
|
||||
{/* Connector line - extends from badge through content to next step */}
|
||||
{!isLast && (
|
||||
<div className="w-0.5 bg-border mt-2 flex-1 min-h-[2rem]" />
|
||||
<div className="mt-2 min-h-[2rem] w-0.5 flex-1 bg-border" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 pt-1 min-w-0">
|
||||
<div className="min-w-0 flex-1 pt-1">
|
||||
<div className="mb-2">
|
||||
<span className="font-semibold text-foreground mr-2">{actor}:</span>{' '}
|
||||
<span className="mr-2 font-semibold text-foreground">{actor}:</span>{' '}
|
||||
<span className="text-muted-foreground">{description}</span>
|
||||
</div>
|
||||
{children && <div className="mt-3 min-w-0">{children}</div>}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getGithubRepoInfo } from '@/lib/github';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { getGithubRepoInfo } from '@/lib/github';
|
||||
|
||||
function formatStars(stars: number) {
|
||||
if (stars >= 1000) {
|
||||
@@ -12,7 +12,7 @@ function formatStars(stars: number) {
|
||||
}
|
||||
|
||||
export function GithubButton() {
|
||||
const [stars, setStars] = useState(4_800);
|
||||
const [stars, setStars] = useState(4800);
|
||||
useEffect(() => {
|
||||
getGithubRepoInfo().then((res) => {
|
||||
if (res?.stargazers_count) {
|
||||
@@ -21,18 +21,18 @@ export function GithubButton() {
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<Button variant={'secondary'} asChild>
|
||||
<Link href="https://git.new/openpanel" className="hidden md:flex">
|
||||
<Button asChild variant={'secondary'}>
|
||||
<Link className="hidden md:flex" href="https://git.new/openpanel">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
aria-hidden="true"
|
||||
className="h-5 w-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{formatStars(stars)} stars
|
||||
|
||||
@@ -33,32 +33,32 @@ export function GuideCard({
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
className="col overflow-hidden rounded-lg border bg-background-light transition-all duration-300 hover:scale-105 hover:shadow-background-dark hover:shadow-lg"
|
||||
href={url}
|
||||
key={url}
|
||||
className="border rounded-lg overflow-hidden bg-background-light col hover:scale-105 transition-all duration-300 hover:shadow-lg hover:shadow-background-dark"
|
||||
>
|
||||
<Image
|
||||
src={cover}
|
||||
alt={title}
|
||||
width={323}
|
||||
height={181}
|
||||
className="w-full"
|
||||
height={181}
|
||||
src={cover}
|
||||
width={323}
|
||||
/>
|
||||
<span className="p-4 col flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="col flex-1 p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span
|
||||
className={`font-mono text-xs px-2 py-1 rounded ${difficultyColors[difficulty]}`}
|
||||
className={`rounded px-2 py-1 font-mono text-xs ${difficultyColors[difficulty]}`}
|
||||
>
|
||||
{difficultyLabels[difficulty]}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{timeToComplete} min
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-1 mb-6">
|
||||
<h2 className="text-xl font-semibold">{title}</h2>
|
||||
<span className="mb-6 flex-1">
|
||||
<h2 className="font-semibold text-xl">{title}</h2>
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{[team, date.toLocaleDateString()].filter(Boolean).join(' · ')}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
@@ -7,7 +7,7 @@ const variantB = [28, 30, 32, 35, 38, 37, 40, 42, 44, 43, 47, 50];
|
||||
|
||||
export function ConversionsIllustration() {
|
||||
return (
|
||||
<div className="h-full col gap-3 px-4 pb-3 pt-5">
|
||||
<div className="col h-full gap-3 px-4 pt-5 pb-3">
|
||||
{/* A/B variant cards */}
|
||||
<div className="row gap-3">
|
||||
<div className="col flex-1 gap-1 rounded-xl border bg-card p-3 transition-all duration-300 group-hover:-translate-y-0.5">
|
||||
@@ -30,7 +30,7 @@ export function ConversionsIllustration() {
|
||||
Variant B ↑
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold font-mono text-xl text-emerald-500">
|
||||
<span className="font-bold font-mono text-emerald-500 text-xl">
|
||||
41.2%
|
||||
</span>
|
||||
<SimpleChart
|
||||
@@ -44,7 +44,7 @@ export function ConversionsIllustration() {
|
||||
|
||||
{/* Breakdown label */}
|
||||
<div className="col gap-1 rounded-xl border bg-card/60 px-3 py-2.5">
|
||||
<span className="text-[9px] uppercase tracking-wider text-muted-foreground">
|
||||
<span className="text-[9px] text-muted-foreground uppercase tracking-wider">
|
||||
Breakdown by experiment variant
|
||||
</span>
|
||||
<div className="row items-center gap-2">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
type IllustrationProps = {
|
||||
className?: string;
|
||||
};
|
||||
@@ -12,15 +10,8 @@ export function DataOwnershipIllustration({
|
||||
{/* 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">
|
||||
<div className="col-span-3 rounded-2xl border border-border bg-card/80 p-3 shadow-xl backdrop-blur transition-all duration-300 group-hover:-translate-x-0.5 group-hover:-translate-y-1 sm:p-4">
|
||||
<div className="flex items-center justify-between text-foreground text-xs">
|
||||
<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" />
|
||||
@@ -31,15 +22,15 @@ export function DataOwnershipIllustration({
|
||||
{/* "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">
|
||||
<div className="flex-1 rounded-xl border border-border bg-card/80 px-3 py-2">
|
||||
<p className="text-[10px] text-muted-foreground">Region</p>
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
<p className="font-medium text-foreground text-xs">
|
||||
EU / Custom
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 rounded-xl bg-card/80 border border-border px-3 py-2">
|
||||
<div className="flex-1 rounded-xl border border-border bg-card/80 px-3 py-2">
|
||||
<p className="text-[10px] text-muted-foreground">Retention</p>
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
<p className="font-medium text-foreground text-xs">
|
||||
Configurable
|
||||
</p>
|
||||
</div>
|
||||
@@ -74,15 +65,8 @@ export function DataOwnershipIllustration({
|
||||
</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>
|
||||
<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-x-0.5 group-hover:translate-y-1 group-hover:opacity-70">
|
||||
<p className="mb-2 text-muted-foreground text-xs">or use our cloud</p>
|
||||
|
||||
<ul className="space-y-1.5">
|
||||
<li className="flex items-center gap-1.5">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
type IllustrationProps = {
|
||||
className?: string;
|
||||
};
|
||||
@@ -10,15 +8,8 @@ export function PrivacyIllustration({ className = '' }: IllustrationProps) {
|
||||
{/* 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">
|
||||
<div className="absolute top-0 right-10 bottom-10 left-0 rounded-2xl border border-border/80 bg-card/70 shadow-lg backdrop-blur-sm 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-muted-foreground text-xs">
|
||||
<span>Session duration</span>
|
||||
<span className="flex items-center gap-1">
|
||||
3m 12s
|
||||
@@ -29,45 +20,37 @@ export function PrivacyIllustration({ className = '' }: IllustrationProps) {
|
||||
{/* Simple line chart */}
|
||||
<div className="mt-3 px-4">
|
||||
<svg
|
||||
viewBox="0 0 120 40"
|
||||
className="h-16 w-full text-muted-foreground"
|
||||
viewBox="0 0 120 40"
|
||||
>
|
||||
<path
|
||||
className="opacity-60"
|
||||
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"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<circle cx="118" cy="14" r="2.5" className="fill-blue-400" />
|
||||
<circle className="fill-blue-400" cx="118" cy="14" r="2.5" />
|
||||
</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">
|
||||
<div className="col absolute top-10 right-0 bottom-0 left-4 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-foreground text-xs">
|
||||
<span>Anonymous visitors</span>
|
||||
<span className="text-[10px] rounded-full bg-card px-2 py-0.5 text-muted-foreground">
|
||||
<span className="rounded-full bg-card px-2 py-0.5 text-[10px] 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">
|
||||
<p className="mb-1 text-[11px] text-muted-foreground">
|
||||
Active now
|
||||
</p>
|
||||
<p className="text-2xl font-semibold text-foreground">128</p>
|
||||
<p className="font-semibold text-2xl text-foreground">128</p>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-[10px] text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -82,12 +65,12 @@ export function PrivacyIllustration({ className = '' }: IllustrationProps) {
|
||||
</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">
|
||||
<div className="mt-auto flex gap-2 border-border border-t px-3 py-2.5 text-[11px]">
|
||||
<div className="flex flex-1 items-center justify-between rounded-xl bg-card/90 px-3 py-1.5">
|
||||
<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">
|
||||
<div className="flex flex-1 items-center justify-between rounded-xl bg-card/90 px-3 py-1.5">
|
||||
<span className="text-muted-foreground">Organic</span>
|
||||
<span className="text-foreground">58%</span>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
'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();
|
||||
@@ -12,7 +8,7 @@ function useFunnelSteps() {
|
||||
{
|
||||
id: 'Visitors',
|
||||
label: 'Visitors',
|
||||
value: 10000,
|
||||
value: 10_000,
|
||||
percentage: 100,
|
||||
color: resolvedTheme === 'dark' ? '#333' : '#888',
|
||||
},
|
||||
@@ -46,7 +42,6 @@ export const PartLabel = ({ part }: { part: any }) => {
|
||||
return (
|
||||
<g transform={`translate(${part.x}, ${part.y})`}>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
style={{
|
||||
fill: resolvedTheme === 'dark' ? '#fff' : '#000',
|
||||
@@ -54,6 +49,7 @@ export const PartLabel = ({ part }: { part: any }) => {
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{part.data.label}
|
||||
</text>
|
||||
@@ -77,24 +73,24 @@ function FunnelVisualization() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<div className="h-full w-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}
|
||||
beforeSeparatorLength={0}
|
||||
beforeSeparatorOffset={0}
|
||||
currentPartSizeExtension={5}
|
||||
borderWidth={20}
|
||||
colors={colors}
|
||||
currentBorderWidth={15}
|
||||
tooltip={() => null}
|
||||
currentPartSizeExtension={5}
|
||||
data={nivoData}
|
||||
direction="horizontal"
|
||||
enableAfterSeparators={false}
|
||||
enableBeforeSeparators={false}
|
||||
layers={['parts', Labels]}
|
||||
margin={{ top: 20, right: 0, bottom: 20, left: 0 }}
|
||||
shapeBlending={0.6}
|
||||
tooltip={() => null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,36 +20,36 @@ function cellStyle(v: number | null) {
|
||||
const opacity = 0.12 + (v / 100) * 0.7;
|
||||
return {
|
||||
backgroundColor: `rgba(34, 197, 94, ${opacity})`,
|
||||
borderColor: `rgba(34, 197, 94, 0.3)`,
|
||||
borderColor: 'rgba(34, 197, 94, 0.3)',
|
||||
color: v > 55 ? 'rgba(0,0,0,0.75)' : 'var(--foreground)',
|
||||
};
|
||||
}
|
||||
|
||||
export function RetentionIllustration() {
|
||||
return (
|
||||
<div className="h-full px-4 pb-3 pt-5">
|
||||
<div className="h-full px-4 pt-5 pb-3">
|
||||
<div className="col h-full gap-1.5">
|
||||
<div className="row gap-1">
|
||||
<div className="w-12 shrink-0" />
|
||||
{headers.map((h) => (
|
||||
<div
|
||||
key={h}
|
||||
className="flex-1 text-center text-[9px] text-muted-foreground"
|
||||
key={h}
|
||||
>
|
||||
{h}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{cohorts.map(({ label, values }) => (
|
||||
<div key={label} className="row flex-1 gap-1">
|
||||
<div className="row flex-1 gap-1" key={label}>
|
||||
<div className="flex w-12 shrink-0 items-center text-[9px] text-muted-foreground">
|
||||
{label}
|
||||
</div>
|
||||
{values.map((v, i) => (
|
||||
<div
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static data
|
||||
className="flex flex-1 items-center justify-center rounded border font-medium text-[9px] transition-all duration-300 group-hover:scale-[1.03]"
|
||||
key={i}
|
||||
className="flex flex-1 items-center justify-center rounded border text-[9px] font-medium transition-all duration-300 group-hover:scale-[1.03]"
|
||||
style={cellStyle(v)}
|
||||
>
|
||||
{v !== null ? `${v}%` : '—'}
|
||||
|
||||
@@ -13,20 +13,22 @@ const referrers = [
|
||||
|
||||
export function RevenueIllustration() {
|
||||
return (
|
||||
<div className="h-full col gap-3 px-4 pb-3 pt-5">
|
||||
<div className="col h-full gap-3 px-4 pt-5 pb-3">
|
||||
{/* MRR stat + chart */}
|
||||
<div className="row gap-3">
|
||||
<div className="col gap-1 rounded-xl border bg-card p-3 transition-all duration-300 group-hover:-translate-y-0.5">
|
||||
<span className="text-[9px] uppercase tracking-wider text-muted-foreground">
|
||||
<span className="text-[9px] text-muted-foreground uppercase tracking-wider">
|
||||
MRR
|
||||
</span>
|
||||
<span className="font-bold font-mono text-xl text-emerald-500">
|
||||
<span className="font-bold font-mono text-emerald-500 text-xl">
|
||||
$8,420
|
||||
</span>
|
||||
<span className="text-[9px] text-emerald-500">↑ 12% this month</span>
|
||||
</div>
|
||||
<div className="col flex-1 gap-1 rounded-xl border bg-card px-3 py-2">
|
||||
<span className="text-[9px] text-muted-foreground">MRR over time</span>
|
||||
<span className="text-[9px] text-muted-foreground">
|
||||
MRR over time
|
||||
</span>
|
||||
<SimpleChart
|
||||
className="mt-1 flex-1"
|
||||
height={36}
|
||||
@@ -39,29 +41,29 @@ export function RevenueIllustration() {
|
||||
|
||||
{/* Revenue by referrer */}
|
||||
<div className="flex-1 overflow-hidden rounded-xl border bg-card">
|
||||
<div className="row border-b border-border px-3 py-1.5">
|
||||
<span className="flex-1 text-[8px] uppercase tracking-wider text-muted-foreground">
|
||||
<div className="row border-border border-b px-3 py-1.5">
|
||||
<span className="flex-1 text-[8px] text-muted-foreground uppercase tracking-wider">
|
||||
Referrer
|
||||
</span>
|
||||
<span className="text-[8px] uppercase tracking-wider text-muted-foreground">
|
||||
<span className="text-[8px] text-muted-foreground uppercase tracking-wider">
|
||||
Revenue
|
||||
</span>
|
||||
</div>
|
||||
{referrers.map((r) => (
|
||||
<div
|
||||
className="row items-center gap-2 border-b border-border/50 px-3 py-1.5 last:border-0"
|
||||
className="row items-center gap-2 border-border/50 border-b px-3 py-1.5 last:border-0"
|
||||
key={r.name}
|
||||
>
|
||||
<span className="text-[9px] text-muted-foreground flex-none w-20 truncate">
|
||||
<span className="w-20 flex-none truncate text-[9px] text-muted-foreground">
|
||||
{r.name}
|
||||
</span>
|
||||
<div className="flex-1 h-1 rounded-full bg-muted overflow-hidden">
|
||||
<div className="h-1 flex-1 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-1 rounded-full bg-emerald-500/70"
|
||||
style={{ width: `${r.pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono text-[9px] text-emerald-500 flex-none">
|
||||
<span className="flex-none font-mono text-[9px] text-emerald-500">
|
||||
{r.amount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,10 @@ import { PlayIcon } from 'lucide-react';
|
||||
|
||||
export function SessionReplayIllustration() {
|
||||
return (
|
||||
<div className="h-full px-6 pb-3 pt-4">
|
||||
<div className="h-full px-6 pt-4 pb-3">
|
||||
<div className="col h-full overflow-hidden rounded-xl border border-border bg-background shadow-lg transition-transform duration-300 group-hover:-translate-y-0.5">
|
||||
{/* Browser chrome */}
|
||||
<div className="row shrink-0 items-center gap-1.5 border-b border-border bg-muted/30 px-3 py-2">
|
||||
<div className="row shrink-0 items-center gap-1.5 border-border border-b bg-muted/30 px-3 py-2">
|
||||
<div className="h-2 w-2 rounded-full bg-red-400" />
|
||||
<div className="h-2 w-2 rounded-full bg-yellow-400" />
|
||||
<div className="h-2 w-2 rounded-full bg-green-400" />
|
||||
@@ -26,16 +26,10 @@ export function SessionReplayIllustration() {
|
||||
<div className="h-2 w-24 rounded-full bg-muted/20" />
|
||||
|
||||
{/* Click heatspot */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{ left: '62%', top: '48%' }}
|
||||
>
|
||||
<div className="absolute" style={{ left: '62%', top: '48%' }}>
|
||||
<div className="h-4 w-4 animate-pulse rounded-full border-2 border-blue-500/70 bg-blue-500/20" />
|
||||
</div>
|
||||
<div
|
||||
className="absolute"
|
||||
style={{ left: '25%', top: '32%' }}
|
||||
>
|
||||
<div className="absolute" style={{ left: '25%', top: '32%' }}>
|
||||
<div className="h-2.5 w-2.5 rounded-full border border-blue-500/40 bg-blue-500/25" />
|
||||
</div>
|
||||
|
||||
@@ -71,11 +65,11 @@ export function SessionReplayIllustration() {
|
||||
</div>
|
||||
|
||||
{/* Playback bar */}
|
||||
<div className="row shrink-0 items-center gap-2 border-t border-border bg-muted/20 px-3 py-2">
|
||||
<div className="row shrink-0 items-center gap-2 border-border border-t bg-muted/20 px-3 py-2">
|
||||
<PlayIcon className="size-3 shrink-0 text-muted-foreground" />
|
||||
<div className="relative flex-1 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<div className="relative h-1 flex-1 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="absolute left-0 top-0 h-1 rounded-full bg-blue-500"
|
||||
className="absolute top-0 left-0 h-1 rounded-full bg-blue-500"
|
||||
style={{ width: '42%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,21 @@ const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
|
||||
const STATS = [
|
||||
{ label: 'Visitors', value: 4128, formatted: null, change: 12, up: true },
|
||||
{ label: 'Page views', value: 12438, formatted: '12.4k', change: 8, up: true },
|
||||
{
|
||||
label: 'Page views',
|
||||
value: 12_438,
|
||||
formatted: '12.4k',
|
||||
change: 8,
|
||||
up: true,
|
||||
},
|
||||
{ label: 'Bounce rate', value: null, formatted: '42%', change: 3, up: false },
|
||||
{ label: 'Avg. session', value: null, formatted: '3m 23s', change: 5, up: true },
|
||||
{
|
||||
label: 'Avg. session',
|
||||
value: null,
|
||||
formatted: '3m 23s',
|
||||
change: 5,
|
||||
up: true,
|
||||
},
|
||||
];
|
||||
|
||||
const SOURCES = [
|
||||
@@ -38,7 +50,9 @@ function AreaChart({ data }: { data: number[] }) {
|
||||
const h = 64;
|
||||
const xStep = w / (data.length - 1);
|
||||
const pts = data.map((v, i) => ({ x: i * xStep, y: h - (v / max) * h }));
|
||||
const line = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`).join(' ');
|
||||
const line = pts
|
||||
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x},${p.y}`)
|
||||
.join(' ');
|
||||
const area = `${line} L ${w},${h} L 0,${h} Z`;
|
||||
const last = pts[pts.length - 1];
|
||||
|
||||
@@ -87,7 +101,7 @@ export function WebAnalyticsIllustration() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="aspect-video col gap-2.5 p-5">
|
||||
<div className="col aspect-video gap-2.5 p-5">
|
||||
{/* Header */}
|
||||
<div className="row items-center justify-between">
|
||||
<div className="row items-center gap-1.5">
|
||||
@@ -95,7 +109,7 @@ export function WebAnalyticsIllustration() {
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||
</span>
|
||||
<span className="text-[10px] font-medium text-muted-foreground">
|
||||
<span className="font-medium text-[10px] text-muted-foreground">
|
||||
<NumberFlow value={liveVisitors} /> online now
|
||||
</span>
|
||||
</div>
|
||||
@@ -111,7 +125,9 @@ export function WebAnalyticsIllustration() {
|
||||
className="col gap-0.5 rounded-lg border bg-card px-2 py-1.5"
|
||||
key={stat.label}
|
||||
>
|
||||
<span className="text-[8px] text-muted-foreground">{stat.label}</span>
|
||||
<span className="text-[8px] text-muted-foreground">
|
||||
{stat.label}
|
||||
</span>
|
||||
<span className="font-mono font-semibold text-xs leading-tight">
|
||||
{stat.formatted ??
|
||||
(stat.value !== null ? (
|
||||
@@ -128,8 +144,10 @@ export function WebAnalyticsIllustration() {
|
||||
</div>
|
||||
|
||||
{/* Area chart */}
|
||||
<div className="flex-1 col gap-1 overflow-hidden rounded-xl border bg-card px-3 pt-2 pb-1">
|
||||
<span className="text-[8px] text-muted-foreground">Unique visitors</span>
|
||||
<div className="col flex-1 gap-1 overflow-hidden rounded-xl border bg-card px-3 pt-2 pb-1">
|
||||
<span className="text-[8px] text-muted-foreground">
|
||||
Unique visitors
|
||||
</span>
|
||||
<AreaChart data={VISITOR_DATA} />
|
||||
<div className="row justify-between px-0.5">
|
||||
{DAYS.map((d) => (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Thank you: https://ui.aceternity.com/components/infinite-moving-cards
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const InfiniteMovingCards = <T,>({
|
||||
items,
|
||||
@@ -47,12 +47,12 @@ export const InfiniteMovingCards = <T,>({
|
||||
if (direction === 'left') {
|
||||
containerRef.current.style.setProperty(
|
||||
'--animation-direction',
|
||||
'forwards',
|
||||
'forwards'
|
||||
);
|
||||
} else {
|
||||
containerRef.current.style.setProperty(
|
||||
'--animation-direction',
|
||||
'reverse',
|
||||
'reverse'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,23 +71,23 @@ export const InfiniteMovingCards = <T,>({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'scroller relative z-20 overflow-hidden -ml-4 md:-ml-[1200px] w-screen md:w-[calc(100vw+1400px)]',
|
||||
className,
|
||||
'scroller relative z-20 -ml-4 w-screen overflow-hidden md:-ml-[1200px] md:w-[calc(100vw+1400px)]',
|
||||
className
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<ul
|
||||
ref={scrollerRef}
|
||||
className={cn(
|
||||
'flex min-w-full shrink-0 gap-8 py-4 w-max flex-nowrap items-start',
|
||||
'flex w-max min-w-full shrink-0 flex-nowrap items-start gap-8 py-4',
|
||||
start && 'animate-scroll',
|
||||
pauseOnHover && 'hover:[animation-play-state:paused]',
|
||||
pauseOnHover && 'hover:[animation-play-state:paused]'
|
||||
)}
|
||||
ref={scrollerRef}
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
<li
|
||||
className="w-[310px] max-w-full relative shrink-0 md:w-[400px]"
|
||||
className="relative w-[310px] max-w-full shrink-0 md:w-[400px]"
|
||||
key={idx.toString()}
|
||||
>
|
||||
{renderItem(item, idx)}
|
||||
|
||||
@@ -3,29 +3,29 @@ import { cn } from '@/lib/utils';
|
||||
export function Logo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={cn('w-16 text-black dark:text-white', className)}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 61 35"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn('text-black dark:text-white w-16', className)}
|
||||
>
|
||||
<rect
|
||||
x="34.0269"
|
||||
y="0.368164"
|
||||
width="10.3474"
|
||||
height="34.2258"
|
||||
rx="5.17372"
|
||||
width="10.3474"
|
||||
x="34.0269"
|
||||
y="0.368164"
|
||||
/>
|
||||
<rect
|
||||
x="49.9458"
|
||||
y="0.368164"
|
||||
width="10.3474"
|
||||
height="17.5109"
|
||||
rx="5.17372"
|
||||
width="10.3474"
|
||||
x="49.9458"
|
||||
y="0.368164"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.212 0C6.36293 0 0 6.36293 0 14.212V20.02C0 27.8691 6.36293 34.232 14.212 34.232C22.0611 34.232 28.424 27.8691 28.424 20.02V14.212C28.424 6.36293 22.0611 0 14.212 0ZM14.2379 8.35999C11.3805 8.35999 9.06419 10.6763 9.06419 13.5337V20.6971C9.06419 23.5545 11.3805 25.8708 14.2379 25.8708C17.0953 25.8708 19.4116 23.5545 19.4116 20.6971V13.5337C19.4116 10.6763 17.0953 8.35999 14.2379 8.35999Z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import type React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
type PerkIcon = LucideIcon | React.ComponentType<{ className?: string }>;
|
||||
|
||||
export function Perks({
|
||||
perks,
|
||||
className,
|
||||
}: { perks: { text: string; icon: PerkIcon }[]; className?: string }) {
|
||||
}: {
|
||||
perks: { text: string; icon: PerkIcon }[];
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<ul className={cn('grid grid-cols-2 gap-2', className)}>
|
||||
{perks.map((perk) => (
|
||||
<li key={perk.text} className="text-sm text-muted-foreground">
|
||||
<perk.icon className="size-4 inline-block mr-2 relative -top-px" />
|
||||
<li className="text-muted-foreground text-sm" key={perk.text}>
|
||||
<perk.icon className="relative -top-px mr-2 inline-block size-4" />
|
||||
{perk.text}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
import NumberFlow from '@number-flow/react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PRICING } from '@openpanel/payments/prices';
|
||||
import { useState } from 'react';
|
||||
import { Slider } from './ui/slider';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function PricingSlider() {
|
||||
const [index, setIndex] = useState(2);
|
||||
@@ -14,15 +13,15 @@ export function PricingSlider() {
|
||||
return (
|
||||
<>
|
||||
<Slider
|
||||
value={[index]}
|
||||
max={PRICING.length}
|
||||
onValueChange={(value) => setIndex(value[0])}
|
||||
step={1}
|
||||
tooltip={
|
||||
match
|
||||
? `${formatNumber(match.events)} events per month`
|
||||
: `More than ${formatNumber(PRICING[PRICING.length - 1].events)} events`
|
||||
}
|
||||
onValueChange={(value) => setIndex(value[0])}
|
||||
value={[index]}
|
||||
/>
|
||||
|
||||
{match ? (
|
||||
@@ -30,7 +29,6 @@ export function PricingSlider() {
|
||||
<div>
|
||||
<NumberFlow
|
||||
className="text-5xl"
|
||||
value={match.price}
|
||||
format={{
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
@@ -38,13 +36,14 @@ export function PricingSlider() {
|
||||
maximumFractionDigits: 1,
|
||||
}}
|
||||
locales={'en-US'}
|
||||
value={match.price}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground ml-2">/ month</span>
|
||||
<span className="ml-2 text-muted-foreground text-sm">/ month</span>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'text-sm text-muted-foreground italic opacity-100',
|
||||
match.price === 0 && 'opacity-0',
|
||||
'text-muted-foreground text-sm italic opacity-100',
|
||||
match.price === 0 && 'opacity-0'
|
||||
)}
|
||||
>
|
||||
+ VAT if applicable
|
||||
|
||||
@@ -20,8 +20,7 @@ export function ScrollTracker() {
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight =
|
||||
document.documentElement.scrollHeight - window.innerHeight;
|
||||
const percent =
|
||||
docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
||||
const percent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
||||
|
||||
if (percent >= 50) {
|
||||
hasFired.current = true;
|
||||
|
||||
@@ -11,7 +11,7 @@ export function Section({
|
||||
id?: string;
|
||||
}) {
|
||||
return (
|
||||
<section id={id} className={cn('my-32 col', className)} {...props}>
|
||||
<section className={cn('col my-32', className)} id={id} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
@@ -47,7 +47,7 @@ export function SectionHeader({
|
||||
align === 'center'
|
||||
? 'center-center text-center'
|
||||
: 'items-start text-left',
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label && <SectionLabel>{label}</SectionLabel>}
|
||||
@@ -55,7 +55,7 @@ export function SectionHeader({
|
||||
{title}
|
||||
</Heading>
|
||||
{description && (
|
||||
<p className={cn('text-muted-foreground max-w-3xl')}>{description}</p>
|
||||
<p className={cn('max-w-3xl text-muted-foreground')}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -71,8 +71,8 @@ export function SectionLabel({
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs uppercase tracking-wider text-muted-foreground font-medium',
|
||||
className,
|
||||
'font-medium text-muted-foreground text-xs uppercase tracking-wider',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface SimpleChartProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
@@ -18,7 +16,9 @@ export function SimpleChart({
|
||||
className,
|
||||
}: SimpleChartProps) {
|
||||
// Skip if no points
|
||||
if (!points.length) return null;
|
||||
if (!points.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate scaling factors
|
||||
const maxValue = Math.max(...points);
|
||||
@@ -45,8 +45,8 @@ export function SimpleChart({
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
className={`w-full ${className ?? ''}`}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
|
||||
const tagVariants = cva(
|
||||
'shadow-sm px-4 gap-2 center-center border self-auto text-xs rounded-full h-7',
|
||||
'center-center h-7 gap-2 self-auto rounded-full border px-4 text-xs shadow-sm',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
light:
|
||||
'bg-background-light dark:bg-background-dark text-muted-foreground',
|
||||
dark: 'bg-foreground-light dark:bg-foreground-dark text-muted border-background/10 shadow-background/5',
|
||||
'bg-background-light text-muted-foreground dark:bg-background-dark',
|
||||
dark: 'border-background/10 bg-foreground-light text-muted shadow-background/5 dark:bg-foreground-dark',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'light',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface TagProps
|
||||
|
||||
@@ -10,20 +10,20 @@ interface Props {
|
||||
export const Toc: React.FC<Props> = ({ toc }) => {
|
||||
return (
|
||||
<FeatureCardContainer className="gap-2">
|
||||
<span className="text-lg font-semibold">Table of contents</span>
|
||||
<span className="font-semibold text-lg">Table of contents</span>
|
||||
<ul>
|
||||
{toc.map((item) => (
|
||||
<li
|
||||
key={item.url}
|
||||
className="py-1"
|
||||
key={item.url}
|
||||
style={{ marginLeft: `${(item.depth - 2) * (4 * 4)}px` }}
|
||||
>
|
||||
<Link
|
||||
className="row group/toc-item items-center gap-2 hover:underline"
|
||||
href={item.url}
|
||||
className="hover:underline row gap-2 items-center group/toc-item"
|
||||
title={item.title?.toString() ?? ''}
|
||||
>
|
||||
<ArrowRightIcon className="shrink-0 w-4 h-4 opacity-30 group-hover/toc-item:opacity-100 transition-opacity" />
|
||||
<ArrowRightIcon className="h-4 w-4 shrink-0 opacity-30 transition-opacity group-hover/toc-item:opacity-100" />
|
||||
<span className="truncate text-sm">{item.title}</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
BadgeIcon,
|
||||
CheckCheckIcon,
|
||||
CheckIcon,
|
||||
HeartIcon,
|
||||
MessageCircleIcon,
|
||||
@@ -36,7 +35,7 @@ export function TwitterCard({
|
||||
|
||||
if (Array.isArray(content) && typeof content[0] === 'string') {
|
||||
return content.map((line) => (
|
||||
<p key={line} className="text-sm">
|
||||
<p className="text-sm" key={line}>
|
||||
{line}
|
||||
</p>
|
||||
));
|
||||
@@ -46,11 +45,11 @@ export function TwitterCard({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border rounded-3xl p-8 col gap-4 bg-background-light">
|
||||
<div className="col gap-4 rounded-3xl border bg-background-light p-8">
|
||||
<div className="row gap-4">
|
||||
<div className="size-12 rounded-full bg-muted overflow-hidden shrink-0">
|
||||
<div className="size-12 shrink-0 overflow-hidden rounded-full bg-muted">
|
||||
{avatarUrl && (
|
||||
<Image src={avatarUrl} alt={name} width={48} height={48} />
|
||||
<Image alt={name} height={48} src={avatarUrl} width={48} />
|
||||
)}
|
||||
</div>
|
||||
<div className="col gap-4">
|
||||
@@ -58,9 +57,9 @@ export function TwitterCard({
|
||||
<div className="">
|
||||
<span className="font-medium">{name}</span>
|
||||
{verified && (
|
||||
<div className="relative inline-block top-0.5 ml-1">
|
||||
<div className="relative top-0.5 ml-1 inline-block">
|
||||
<BadgeIcon className="size-4 fill-[#1D9BF0] text-[#1D9BF0]" />
|
||||
<div className="absolute inset-0 center-center">
|
||||
<div className="center-center absolute inset-0">
|
||||
<CheckIcon className="size-2 text-white" strokeWidth={3} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,15 +72,15 @@ export function TwitterCard({
|
||||
{renderContent()}
|
||||
<div className="row gap-4 text-muted-foreground text-sm">
|
||||
<div className="row gap-2">
|
||||
<MessageCircleIcon className="transition-all size-4 fill-background hover:fill-blue-500 hover:text-blue-500" />
|
||||
<MessageCircleIcon className="size-4 fill-background transition-all hover:fill-blue-500 hover:text-blue-500" />
|
||||
{/* <span>{replies}</span> */}
|
||||
</div>
|
||||
<div className="row gap-2">
|
||||
<RefreshCwIcon className="transition-all size-4 fill-background hover:text-blue-500" />
|
||||
<RefreshCwIcon className="size-4 fill-background transition-all hover:text-blue-500" />
|
||||
{/* <span>{retweets}</span> */}
|
||||
</div>
|
||||
<div className="row gap-2">
|
||||
<HeartIcon className="transition-all size-4 fill-background hover:fill-rose-500 hover:text-rose-500" />
|
||||
<HeartIcon className="size-4 fill-background transition-all hover:fill-rose-500 hover:text-rose-500" />
|
||||
{/* <span>{likes}</span> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { FeatureCardBackground } from '../feature-card';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
@@ -15,8 +14,8 @@ const AccordionItem = ({
|
||||
ref?: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Item>>;
|
||||
}) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn('border-b last:border-b-0', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -30,13 +29,13 @@ const AccordionTrigger = ({
|
||||
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
|
||||
ref?: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Trigger>>;
|
||||
}) => (
|
||||
<AccordionPrimitive.Header className="flex not-prose">
|
||||
<AccordionPrimitive.Header className="not-prose flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'group relative overflow-hidden flex flex-1 items-center justify-between py-4 font-medium transition-all [&[data-state=open]>svg]:rotate-180 cursor-pointer',
|
||||
className,
|
||||
'group relative flex flex-1 cursor-pointer items-center justify-between overflow-hidden py-4 font-medium transition-all [&[data-state=open]>svg]:rotate-180',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<FeatureCardBackground />
|
||||
@@ -56,14 +55,14 @@ const AccordionContent = ({
|
||||
ref?: React.RefObject<React.ElementRef<typeof AccordionPrimitive.Content>>;
|
||||
}) => (
|
||||
<AccordionPrimitive.Content
|
||||
className="overflow-hidden text-muted-foreground transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
ref={ref}
|
||||
className="overflow-hidden transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down text-muted-foreground"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'pb-4 pt-0 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
||||
className,
|
||||
'pt-0 pb-4 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'hover:-translate-y-px inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-all hover:-translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -19,7 +18,7 @@ const buttonVariants = cva(
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
naked:
|
||||
'bg-transparent hover:bg-transparent ring-0 border-none !px-0 !py-0 shadow-none',
|
||||
'!px-0 !py-0 border-none bg-transparent shadow-none ring-0 hover:bg-transparent',
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 px-4',
|
||||
@@ -32,7 +31,7 @@ const buttonVariants = cva(
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const inputVariants = cva(
|
||||
'flex w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
|
||||
'flex w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background transition-colors file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
@@ -16,7 +15,7 @@ const inputVariants = cva(
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface InputProps
|
||||
@@ -28,9 +27,9 @@ export interface InputProps
|
||||
const Input = ({ className, type, size, ref, ...props }: InputProps) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(inputVariants({ size, className }))}
|
||||
ref={ref}
|
||||
type={type}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function useMediaQuery(query: string) {
|
||||
const [matches, setMatches] = React.useState(false);
|
||||
@@ -31,28 +30,28 @@ const Slider = ({
|
||||
return (
|
||||
<>
|
||||
{isMobile && (
|
||||
<div className="text-sm text-muted-foreground mb-4">{tooltip}</div>
|
||||
<div className="mb-4 text-muted-foreground text-sm">{tooltip}</div>
|
||||
)}
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full touch-none select-none items-center',
|
||||
className,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-white/10">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-white/90" />
|
||||
</SliderPrimitive.Track>
|
||||
{tooltip && !isMobile ? (
|
||||
<Tooltip open disableHoverableContent>
|
||||
<Tooltip disableHoverableContent open>
|
||||
<TooltipTrigger asChild>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-white bg-black ring-offset-black transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="rounded-full border-white/30 bg-black py-1 text-white/70 text-xs"
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
className="rounded-full bg-black text-white/70 py-1 text-xs border-white/30"
|
||||
>
|
||||
{tooltip}
|
||||
</TooltipContent>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
@@ -20,12 +19,12 @@ const TooltipContent = ({
|
||||
ref?: React.RefObject<React.ElementRef<typeof TooltipPrimitive.Content>>;
|
||||
}) => (
|
||||
<TooltipPrimitive.Content
|
||||
className={cn(
|
||||
'fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 animate-in overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-popover-foreground text-sm shadow-md data-[state=closed]:animate-out',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import Image from 'next/image';
|
||||
import { FeatureCardContainer } from './feature-card';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface WindowImageProps {
|
||||
src?: string;
|
||||
@@ -24,20 +24,20 @@ export function WindowImage({
|
||||
const darkSrc = srcDark || src;
|
||||
const lightSrc = srcLight || src;
|
||||
|
||||
if (!darkSrc || !lightSrc) {
|
||||
if (!(darkSrc && lightSrc)) {
|
||||
throw new Error(
|
||||
'WindowImage requires either src or both srcDark and srcLight',
|
||||
'WindowImage requires either src or both srcDark and srcLight'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FeatureCardContainer
|
||||
className={cn([
|
||||
'overflow-hidden rounded-lg border border-border bg-foreground/10 shadow-lg/5 relative z-10 [@media(min-width:1100px)]:-mx-16 p-4 md:p-16',
|
||||
'relative z-10 overflow-hidden rounded-lg border border-border bg-foreground/10 p-4 shadow-lg/5 md:p-16 [@media(min-width:1100px)]:-mx-16',
|
||||
className,
|
||||
])}
|
||||
>
|
||||
<div className="rounded-lg overflow-hidden p-2 bg-card/80 border col gap-2 relative">
|
||||
<div className="col relative gap-2 overflow-hidden rounded-lg border bg-card/80 p-2">
|
||||
{/* Window controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1.5">
|
||||
@@ -46,25 +46,25 @@ export function WindowImage({
|
||||
<div className="size-2 rounded-full bg-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-full border rounded-md overflow-hidden">
|
||||
<div className="relative w-full overflow-hidden rounded-md border">
|
||||
<Image
|
||||
src={darkSrc}
|
||||
alt={alt}
|
||||
width={1200}
|
||||
className="hidden h-auto w-full dark:block"
|
||||
height={800}
|
||||
className="hidden dark:block w-full h-auto"
|
||||
src={darkSrc}
|
||||
width={1200}
|
||||
/>
|
||||
<Image
|
||||
src={lightSrc}
|
||||
alt={alt}
|
||||
width={1200}
|
||||
className="h-auto w-full dark:hidden"
|
||||
height={800}
|
||||
className="dark:hidden w-full h-auto"
|
||||
src={lightSrc}
|
||||
width={1200}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{caption && (
|
||||
<figcaption className="text-center text-sm text-muted-foreground max-w-lg mx-auto">
|
||||
<figcaption className="mx-auto max-w-lg text-center text-muted-foreground text-sm">
|
||||
{caption}
|
||||
</figcaption>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user