redesign
This commit is contained in:
+26
-33
@@ -4,38 +4,37 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
/* 3blue1brown-inspired dark palette: deep space blue background,
|
/* Monochrome chrome: the shell carries no color of its own, so the
|
||||||
a warm gold accent for emphasis, and a small, deliberate set of
|
five algorithm-state colors below are the only color on the page. */
|
||||||
"state" colors used consistently across every visualization. */
|
--background: 224 15% 6%;
|
||||||
--background: 222 47% 7%;
|
--foreground: 210 20% 95%;
|
||||||
--foreground: 210 40% 96%;
|
|
||||||
|
|
||||||
--primary: 199 89% 64%; /* 3b1b sky blue */
|
--primary: 210 25% 96%; /* near-white, inverted CTA fill */
|
||||||
--primary-foreground: 222 47% 7%;
|
--primary-foreground: 224 20% 9%;
|
||||||
--secondary: 222 30% 16%;
|
--secondary: 224 12% 15%;
|
||||||
--secondary-foreground: 210 40% 90%;
|
--secondary-foreground: 210 15% 90%;
|
||||||
--accent: 43 96% 62%; /* warm gold, used sparingly */
|
--accent: 224 12% 17%; /* neutral hover surface, not a brand color */
|
||||||
--accent-foreground: 222 47% 7%;
|
--accent-foreground: 210 20% 95%;
|
||||||
--destructive: 358 75% 59%;
|
--destructive: 358 65% 55%;
|
||||||
--destructive-foreground: 210 40% 96%;
|
--destructive-foreground: 210 20% 96%;
|
||||||
--muted: 222 25% 14%;
|
--muted: 224 12% 13%;
|
||||||
--muted-foreground: 215 20% 65%;
|
--muted-foreground: 216 10% 62%;
|
||||||
--card: 222 40% 10%;
|
--card: 224 14% 9%;
|
||||||
--card-foreground: 210 40% 96%;
|
--card-foreground: 210 20% 95%;
|
||||||
--popover: 222 44% 9%;
|
--popover: 224 16% 8%;
|
||||||
--popover-foreground: 210 40% 96%;
|
--popover-foreground: 210 20% 95%;
|
||||||
--border: 222 25% 20%;
|
--border: 224 12% 18%;
|
||||||
--input: 222 25% 20%;
|
--input: 224 12% 18%;
|
||||||
--ring: 199 89% 64%;
|
--ring: 205 90% 62%; /* the one deliberate accent: focus only */
|
||||||
--radius: 0.75rem;
|
--radius: 0.75rem;
|
||||||
|
|
||||||
/* Algorithm state colors — one meaning each, used everywhere. */
|
/* Algorithm state colors — one meaning each, used everywhere. */
|
||||||
--state-idle: 199 60% 55%;
|
--state-idle: 205 40% 50%;
|
||||||
--state-compare: 43 96% 62%;
|
--state-compare: 43 90% 60%;
|
||||||
--state-swap: 358 75% 59%;
|
--state-swap: 358 75% 59%;
|
||||||
--state-sorted: 152 60% 50%;
|
--state-sorted: 152 55% 48%;
|
||||||
--state-pivot: 271 76% 68%;
|
--state-pivot: 271 70% 66%;
|
||||||
--state-active-range: 199 89% 64%;
|
--state-active-range: 205 60% 55%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,12 +70,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.glow-primary {
|
|
||||||
box-shadow: 0 0 24px hsl(var(--primary) / 0.35);
|
|
||||||
}
|
|
||||||
.glow-accent {
|
|
||||||
box-shadow: 0 0 24px hsl(var(--accent) / 0.35);
|
|
||||||
}
|
|
||||||
.text-balance {
|
.text-balance {
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,22 +1,22 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Inter, JetBrains_Mono } from 'next/font/google';
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
const fontSans = Inter({
|
const fontSans = Geist({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
variable: '--font-sans',
|
variable: '--font-sans',
|
||||||
});
|
});
|
||||||
|
|
||||||
const fontMono = JetBrains_Mono({
|
const fontMono = Geist_Mono({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
variable: '--font-mono',
|
variable: '--font-mono',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Valgo — Watch how sorting actually works',
|
title: 'Valgo - Watch how sorting actually works',
|
||||||
description: 'An animated, narrated visualizer for nine classic sorting algorithms.',
|
description: 'An animated, narrated visualizer for nine classic sorting algorithms.',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+98
-76
@@ -14,7 +14,7 @@ import StatsPanel from "@/components/StatsPanel";
|
|||||||
import PlaybackControls, { Speed } from "@/components/PlaybackControls";
|
import PlaybackControls, { Speed } from "@/components/PlaybackControls";
|
||||||
import { AlgorithmPicker } from "@/components/Pickers";
|
import { AlgorithmPicker } from "@/components/Pickers";
|
||||||
import SiteFooter from "@/components/SiteFooter";
|
import SiteFooter from "@/components/SiteFooter";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import Panel from "@/components/Panel";
|
||||||
|
|
||||||
const SPEED_MS: Record<Speed, number> = { slow: 260, normal: 120, fast: 55, turbo: 18 };
|
const SPEED_MS: Record<Speed, number> = { slow: 260, normal: 120, fast: 55, turbo: 18 };
|
||||||
|
|
||||||
@@ -22,11 +22,27 @@ function randomArray(size: number): number[] {
|
|||||||
return Array.from({ length: size }, () => Math.floor(Math.random() * 100) + 1);
|
return Array.from({ length: size }, () => Math.floor(Math.random() * 100) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deterministic so the server-rendered array matches the client's first
|
||||||
|
// render; the real random array is generated client-side after mount.
|
||||||
|
function seededArray(size: number): number[] {
|
||||||
|
return Array.from({ length: size }, (_, i) => ((i * 37) % 100) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BrandMark(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 20 20" fill="none" {...props}>
|
||||||
|
<rect x="2" y="11" width="4" height="7" rx="1" fill="currentColor" opacity="0.5" />
|
||||||
|
<rect x="8" y="6" width="4" height="12" rx="1" fill="currentColor" opacity="0.75" />
|
||||||
|
<rect x="14" y="2" width="4" height="16" rx="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [algorithm, setAlgorithm] = useState<AlgorithmKey>("bubble");
|
const [algorithm, setAlgorithm] = useState<AlgorithmKey>("bubble");
|
||||||
const [language, setLanguage] = useState<LanguageCode>("en");
|
const [language, setLanguage] = useState<LanguageCode>("en");
|
||||||
const [arraySize, setArraySize] = useState(24);
|
const [arraySize, setArraySize] = useState(24);
|
||||||
const [baseArray, setBaseArray] = useState<number[]>(() => randomArray(24));
|
const [baseArray, setBaseArray] = useState<number[]>(() => seededArray(24));
|
||||||
const [steps, setSteps] = useState<Step[]>([]);
|
const [steps, setSteps] = useState<Step[]>([]);
|
||||||
const [stepIndex, setStepIndex] = useState(0);
|
const [stepIndex, setStepIndex] = useState(0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
@@ -34,6 +50,11 @@ export default function Page() {
|
|||||||
|
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBaseArray(randomArray(arraySize));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
// (Re)compute the full step-by-step story whenever the algorithm or the
|
// (Re)compute the full step-by-step story whenever the algorithm or the
|
||||||
// underlying data changes. Everything downstream just plays it back.
|
// underlying data changes. Everything downstream just plays it back.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -111,94 +132,95 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<header className="border-b border-border bg-card/30 backdrop-blur-sm">
|
<header className="sticky top-0 z-40 border-b border-border bg-background/85 backdrop-blur-md">
|
||||||
<div className="container mx-auto px-4 md:px-6 py-6 flex flex-wrap items-center justify-between gap-4">
|
<div className="container mx-auto max-w-6xl px-4 md:px-6 h-16 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">
|
<BrandMark className="h-5 w-5 shrink-0 text-foreground" />
|
||||||
<span className="text-primary">{t(language, "brand")}</span>
|
<h1 className="text-base font-semibold tracking-tight truncate">{t(language, "brand")}</h1>
|
||||||
</h1>
|
<span className="hidden md:inline text-sm text-muted-foreground ml-2 pl-3 border-l border-border truncate">
|
||||||
<p className="text-sm text-muted-foreground">{t(language, "tagline")}</p>
|
{t(language, "tagline")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<AlgorithmPicker value={algorithm} onChange={setAlgorithm} triggerLabel={t(language, "pickAlgorithm")} />
|
<AlgorithmPicker value={algorithm} onChange={setAlgorithm} triggerLabel={t(language, "pickAlgorithm")} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 container mx-auto px-4 md:px-6 py-10 space-y-8">
|
<main className="flex-1">
|
||||||
<motion.div
|
<motion.section
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
className="rounded-xl border border-border bg-card/60 backdrop-blur-sm p-6"
|
className="border-b border-border bg-card/30"
|
||||||
>
|
>
|
||||||
<Bars
|
<div className="container mx-auto max-w-6xl px-4 md:px-6 py-8 md:py-10">
|
||||||
values={currentStep?.array ?? baseArray}
|
<Bars
|
||||||
indices={currentStep?.indices ?? []}
|
values={currentStep?.array ?? baseArray}
|
||||||
action={currentStep?.action}
|
indices={currentStep?.indices ?? []}
|
||||||
sorted={currentStep?.sorted ?? []}
|
action={currentStep?.action}
|
||||||
range={currentStep?.range}
|
sorted={currentStep?.sorted ?? []}
|
||||||
pivotIndex={pivotIndex}
|
range={currentStep?.range}
|
||||||
stepDurationMs={SPEED_MS[speed]}
|
pivotIndex={pivotIndex}
|
||||||
/>
|
stepDurationMs={SPEED_MS[speed]}
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
<div className="lg:col-span-2 space-y-6">
|
|
||||||
<Narration text={narration} stepIndex={stepIndex} title={t(language, "narrationTitle")} />
|
|
||||||
<PlaybackControls
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
onTogglePlay={handleTogglePlay}
|
|
||||||
onShuffle={handleShuffle}
|
|
||||||
onReset={handleReset}
|
|
||||||
arraySize={arraySize}
|
|
||||||
onArraySizeChange={handleArraySizeChange}
|
|
||||||
speed={speed}
|
|
||||||
onSpeedChange={setSpeed}
|
|
||||||
stepIndex={stepIndex}
|
|
||||||
stepCount={Math.max(steps.length - 1, 0)}
|
|
||||||
onScrub={handleScrub}
|
|
||||||
labels={{
|
|
||||||
play: t(language, "play"), pause: t(language, "pause"),
|
|
||||||
shuffle: t(language, "shuffle"), reset: t(language, "reset"),
|
|
||||||
arraySize: t(language, "arraySize"), elements: t(language, "elements"),
|
|
||||||
speed: t(language, "speed"), speedSlow: t(language, "speedSlow"),
|
|
||||||
speedNormal: t(language, "speedNormal"), speedFast: t(language, "speedFast"),
|
|
||||||
speedTurbo: t(language, "speedTurbo"),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="container mx-auto max-w-6xl px-4 md:px-6 py-8 md:py-10">
|
||||||
<StatsPanel
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
title={t(language, "statsTitle")}
|
<div className="lg:col-span-2 space-y-6">
|
||||||
comparisons={currentStep?.comparisons ?? 0}
|
<Narration text={narration} stepIndex={stepIndex} title={t(language, "narrationTitle")} />
|
||||||
writes={currentStep?.writes ?? 0}
|
<PlaybackControls
|
||||||
stepLabel={t(language, "step")}
|
isPlaying={isPlaying}
|
||||||
stepIndex={stepIndex}
|
onTogglePlay={handleTogglePlay}
|
||||||
stepCount={Math.max(steps.length - 1, 0)}
|
onShuffle={handleShuffle}
|
||||||
sortedLabel={t(language, "sorted")}
|
onReset={handleReset}
|
||||||
sortedCount={currentStep?.sorted.length ?? 0}
|
arraySize={arraySize}
|
||||||
total={baseArray.length}
|
onArraySizeChange={handleArraySizeChange}
|
||||||
/>
|
speed={speed}
|
||||||
|
onSpeedChange={setSpeed}
|
||||||
|
stepIndex={stepIndex}
|
||||||
|
stepCount={Math.max(steps.length - 1, 0)}
|
||||||
|
onScrub={handleScrub}
|
||||||
|
labels={{
|
||||||
|
play: t(language, "play"), pause: t(language, "pause"),
|
||||||
|
shuffle: t(language, "shuffle"), reset: t(language, "reset"),
|
||||||
|
arraySize: t(language, "arraySize"), elements: t(language, "elements"),
|
||||||
|
speed: t(language, "speed"), speedSlow: t(language, "speedSlow"),
|
||||||
|
speedNormal: t(language, "speedNormal"), speedFast: t(language, "speedFast"),
|
||||||
|
speedTurbo: t(language, "speedTurbo"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{description && (
|
<div className="space-y-6">
|
||||||
<Card className="bg-card/60 backdrop-blur-sm">
|
<StatsPanel
|
||||||
<CardHeader>
|
title={t(language, "statsTitle")}
|
||||||
<CardTitle className="text-xl">{description.title}</CardTitle>
|
comparisons={currentStep?.comparisons ?? 0}
|
||||||
<CardDescription className="space-y-0.5">
|
writes={currentStep?.writes ?? 0}
|
||||||
<div className="font-mono text-xs">{description.timeComplexity}</div>
|
stepLabel={t(language, "step")}
|
||||||
<div className="font-mono text-xs">{description.spaceComplexity}</div>
|
stepIndex={stepIndex}
|
||||||
</CardDescription>
|
stepCount={Math.max(steps.length - 1, 0)}
|
||||||
</CardHeader>
|
sortedLabel={t(language, "sorted")}
|
||||||
<CardContent>
|
sortedCount={currentStep?.sorted.length ?? 0}
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">{description.description}</p>
|
total={baseArray.length}
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="rounded-xl border border-border bg-card/60 backdrop-blur-sm p-5">
|
{description && (
|
||||||
<p className="text-xs uppercase tracking-widest text-muted-foreground mb-1">{t(language, "growthTitle")}</p>
|
<Panel>
|
||||||
<p className="text-xs text-muted-foreground mb-4">{t(language, "growthCaption")}</p>
|
<h2 className="text-xl font-semibold tracking-tight">{description.title}</h2>
|
||||||
<GrowthChart active={cclass} n={arraySize} maxN={100} labels={growthLabels} />
|
<div className="mt-1 space-y-0.5">
|
||||||
|
<div className="font-mono text-xs text-muted-foreground">{description.timeComplexity}</div>
|
||||||
|
<div className="font-mono text-xs text-muted-foreground">{description.spaceComplexity}</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground leading-relaxed">{description.description}</p>
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<p className="text-xs uppercase tracking-widest text-muted-foreground mb-1">{t(language, "growthTitle")}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">{t(language, "growthCaption")}</p>
|
||||||
|
<GrowthChart active={cclass} n={arraySize} maxN={100} labels={growthLabels} />
|
||||||
|
</Panel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ function colorFor(
|
|||||||
if (action === "swap" || action === "shift" || action === "merge-write" || action === "overwrite") return stateColor.swap;
|
if (action === "swap" || action === "shift" || action === "merge-write" || action === "overwrite") return stateColor.swap;
|
||||||
return stateColor.compare;
|
return stateColor.compare;
|
||||||
}
|
}
|
||||||
if (range && i >= range[0] && i <= range[1]) return "hsl(var(--state-range) / 0.55)";
|
if (range && i >= range[0] && i <= range[1]) return "hsl(var(--state-active-range) / 0.55)";
|
||||||
return stateColor.idle;
|
return stateColor.idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import Panel from "@/components/Panel";
|
||||||
|
|
||||||
interface NarrationProps {
|
interface NarrationProps {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -10,7 +11,7 @@ interface NarrationProps {
|
|||||||
|
|
||||||
export default function Narration({ text, stepIndex, title }: NarrationProps) {
|
export default function Narration({ text, stepIndex, title }: NarrationProps) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border bg-card/60 backdrop-blur-sm p-5">
|
<Panel>
|
||||||
<p className="text-xs uppercase tracking-widest text-muted-foreground mb-2">{title}</p>
|
<p className="text-xs uppercase tracking-widest text-muted-foreground mb-2">{title}</p>
|
||||||
<div className="min-h-[3.5rem]">
|
<div className="min-h-[3.5rem]">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
@@ -26,6 +27,6 @@ export default function Narration({ text, stepIndex, title }: NarrationProps) {
|
|||||||
</motion.p>
|
</motion.p>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { HTMLAttributes } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default function Panel({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("rounded-xl border border-border bg-card/60 backdrop-blur-sm p-5 md:p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Play, Pause, Shuffle, RotateCcw } from "lucide-react";
|
import { Play, Pause, Shuffle, RotateCcw } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import Panel from "@/components/Panel";
|
||||||
|
|
||||||
export type Speed = "slow" | "normal" | "fast" | "turbo";
|
export type Speed = "slow" | "normal" | "fast" | "turbo";
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ export default function PlaybackControls({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border bg-card/60 backdrop-blur-sm p-5 space-y-5">
|
<Panel className="space-y-5">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Button onClick={onTogglePlay} size="lg" className="gap-2 min-w-[112px]">
|
<Button onClick={onTogglePlay} size="lg" className="gap-2 min-w-[112px]">
|
||||||
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||||
@@ -95,6 +96,6 @@ export default function PlaybackControls({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ interface SiteFooterProps {
|
|||||||
export default function SiteFooter({ year, rightsLabel, licenseLabel, viewLicenseLabel, lang, onLangChange }: SiteFooterProps) {
|
export default function SiteFooter({ year, rightsLabel, licenseLabel, viewLicenseLabel, lang, onLangChange }: SiteFooterProps) {
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-border bg-card/40 py-6 mt-16">
|
<footer className="border-t border-border bg-card/40 py-6 mt-16">
|
||||||
<div className="container mx-auto px-4 md:px-6 flex flex-wrap items-center justify-between gap-4">
|
<div className="container mx-auto max-w-6xl px-4 md:px-6 flex flex-wrap items-center justify-between gap-4">
|
||||||
<p className="text-sm text-muted-foreground">© {year} Quintavalle Pietro. {rightsLabel}</p>
|
<p className="text-sm text-muted-foreground">© {year} Quintavalle Pietro. {rightsLabel}</p>
|
||||||
<nav className="flex items-center gap-4">
|
<nav className="flex items-center gap-4">
|
||||||
<LanguagePicker value={lang} onChange={onLangChange} />
|
<LanguagePicker value={lang} onChange={onLangChange} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import Panel from "@/components/Panel";
|
||||||
|
|
||||||
interface StatItemProps {
|
interface StatItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -16,7 +17,7 @@ function StatItem({ label, value }: StatItemProps) {
|
|||||||
initial={{ opacity: 0.4 }}
|
initial={{ opacity: 0.4 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
className="text-2xl font-mono tabular-nums text-primary"
|
className="text-2xl font-mono tabular-nums font-semibold text-foreground"
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
@@ -40,7 +41,7 @@ export default function StatsPanel({
|
|||||||
title, comparisons, writes, stepLabel, stepIndex, stepCount, sortedLabel, sortedCount, total,
|
title, comparisons, writes, stepLabel, stepIndex, stepCount, sortedLabel, sortedCount, total,
|
||||||
}: StatsPanelProps) {
|
}: StatsPanelProps) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border bg-card/60 backdrop-blur-sm p-5">
|
<Panel>
|
||||||
<p className="text-xs uppercase tracking-widest text-muted-foreground mb-3">{title}</p>
|
<p className="text-xs uppercase tracking-widest text-muted-foreground mb-3">{title}</p>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<StatItem label="Comparisons" value={comparisons} />
|
<StatItem label="Comparisons" value={comparisons} />
|
||||||
@@ -48,6 +49,6 @@ export default function StatsPanel({
|
|||||||
<StatItem label={stepLabel} value={`${stepIndex} / ${stepCount}`} />
|
<StatItem label={stepLabel} value={`${stepIndex} / ${stepCount}`} />
|
||||||
<StatItem label={sortedLabel} value={`${sortedCount} / ${total}`} />
|
<StatItem label={sortedLabel} value={`${sortedCount} / ${total}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Card.displayName = "Card"
|
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardHeader.displayName = "CardHeader"
|
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<h3
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"text-2xl font-semibold leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardTitle.displayName = "CardTitle"
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<p
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardDescription.displayName = "CardDescription"
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
||||||
))
|
|
||||||
CardContent.displayName = "CardContent"
|
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardFooter.displayName = "CardFooter"
|
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
||||||
Reference in New Issue
Block a user