Retour au catalogue
Changelog Category Filter
Changelog avec filtres par categorie (feature, fix, improvement) et filtrage anime.
changelogmedium Both Responsive a11y
minimalcorporatesaasuniversalstacked
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Sparkles, TrendingUp, Bug, Tag } from "lucide-react";
import type { LucideIcon } from "lucide-react";
interface Category {
id: string;
label: string;
count: number;
}
interface Entry {
title: string;
description: string;
date: string;
category: string;
version: string;
}
interface ChangelogCategoryFilterProps {
title?: string;
subtitle?: string;
categories?: Category[];
entries?: Entry[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
const catIcons: Record<string, LucideIcon> = {
feature: Sparkles,
improvement: TrendingUp,
fix: Bug,
};
const catColors: Record<string, string> = {
feature: "var(--color-accent)",
improvement: "var(--color-foreground)",
fix: "var(--color-foreground-muted)",
};
export default function ChangelogCategoryFilter({
title = "Changelog",
subtitle = "Filtrez par type de changement",
categories = [],
entries = [],
}: ChangelogCategoryFilterProps) {
const [activeFilter, setActiveFilter] = useState("all");
const filtered = activeFilter === "all" ? entries : entries.filter((e) => e.category === activeFilter);
return (
<section style={{ paddingTop: "var(--section-padding-y)", paddingBottom: "var(--section-padding-y)", background: "var(--color-background)" }}>
<div style={{ maxWidth: 760, margin: "0 auto", padding: "0 var(--container-padding-x)" }}>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: EASE }}
style={{ marginBottom: "2rem" }}
>
<h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.75rem, 3vw, 2.5rem)", fontWeight: 700, color: "var(--color-foreground)", marginBottom: "0.5rem" }}>{title}</h2>
<p style={{ fontSize: "1rem", color: "var(--color-foreground-muted)" }}>{subtitle}</p>
</motion.div>
{/* Filter chips */}
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.05, ease: EASE }}
style={{ display: "flex", gap: "0.5rem", marginBottom: "2rem", flexWrap: "wrap" }}
>
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => setActiveFilter(cat.id)}
style={{
display: "flex",
alignItems: "center",
gap: "0.35rem",
padding: "0.4rem 1rem",
borderRadius: "var(--radius-full)",
border: "1px solid",
borderColor: activeFilter === cat.id ? "var(--color-accent)" : "var(--color-border)",
background: activeFilter === cat.id ? "var(--color-accent)" : "var(--color-background)",
color: activeFilter === cat.id ? "var(--color-background)" : "var(--color-foreground-muted)",
fontSize: "0.8125rem",
fontWeight: 600,
cursor: "pointer",
transition: "all 0.2s",
}}
>
{cat.label}
<span style={{ fontSize: "0.6875rem", opacity: 0.7 }}>({cat.count})</span>
</button>
))}
</motion.div>
{/* Entries */}
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<AnimatePresence mode="popLayout">
{filtered.map((entry) => {
const Icon = catIcons[entry.category] ?? Tag;
const color = catColors[entry.category] ?? "var(--color-foreground-muted)";
return (
<motion.div
key={entry.title}
layout
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.3, ease: EASE }}
style={{ display: "flex", gap: "1rem", padding: "1.25rem", borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", alignItems: "start" }}
>
<div style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", background: "var(--color-background-alt)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
<Icon style={{ width: 15, height: 15, color }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.25rem", flexWrap: "wrap" }}>
<h3 style={{ fontSize: "0.9375rem", fontWeight: 600, color: "var(--color-foreground)" }}>{entry.title}</h3>
<span style={{ fontSize: "0.6875rem", fontWeight: 600, padding: "0.1rem 0.5rem", borderRadius: "var(--radius-full)", background: "var(--color-background-alt)", color }}>{entry.version}</span>
</div>
<p style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)", lineHeight: 1.6 }}>{entry.description}</p>
</div>
<span style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)", flexShrink: 0, whiteSpace: "nowrap" }}>{entry.date}</span>
</motion.div>
);
})}
</AnimatePresence>
</div>
</div>
</section>
);
}