Retour au catalogue
Download Comparison Table
Matrice de comparaison des versions telechargeable avec fonctionnalites, prix et boutons de telechargement par tier.
downloadcomplex Both Responsive a11y
corporateminimalsaasuniversalgrid
Theme
"use client";
import { motion } from "framer-motion";
import { Download, Check, X, Crown } from "lucide-react";
import React from "react";
interface Edition {
id: string;
name: string;
price: string;
priceDetail?: string;
downloadUrl: string;
isPopular?: boolean;
}
interface FeatureRow {
name: string;
category?: string;
values: Record<string, boolean | string>;
}
interface DownloadComparisonTableProps {
title?: string;
subtitle?: string;
badge?: string;
editions?: Edition[];
features?: FeatureRow[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
export default function DownloadComparisonTable({
title,
subtitle,
badge,
editions,
features,
}: DownloadComparisonTableProps) {
const resolvedTitle = title ?? "Choisissez votre edition";
const resolvedSubtitle = subtitle ?? "Comparez les fonctionnalites de chaque edition pour trouver celle qui vous convient.";
const resolvedBadge = badge ?? "Editions";
const resolvedEditions: Edition[] = editions ?? [
{ id: "community", name: "Community", price: "Gratuit", priceDetail: "pour toujours", downloadUrl: "#" },
{ id: "pro", name: "Pro", price: "29 EUR", priceDetail: "/ mois", downloadUrl: "#", isPopular: true },
{ id: "enterprise", name: "Enterprise", price: "Sur devis", priceDetail: "personnalise", downloadUrl: "#" },
];
const resolvedFeatures: FeatureRow[] = features ?? [
{ name: "Utilisateurs", category: "General", values: { community: "1", pro: "10", enterprise: "Illimite" } },
{ name: "Stockage", category: "General", values: { community: "5 Go", pro: "100 Go", enterprise: "Illimite" } },
{ name: "Projets", category: "General", values: { community: "3", pro: "Illimite", enterprise: "Illimite" } },
{ name: "API REST", category: "Integration", values: { community: true, pro: true, enterprise: true } },
{ name: "Webhooks", category: "Integration", values: { community: false, pro: true, enterprise: true } },
{ name: "SSO / SAML", category: "Securite", values: { community: false, pro: false, enterprise: true } },
{ name: "Audit logs", category: "Securite", values: { community: false, pro: true, enterprise: true } },
{ name: "Support prioritaire", category: "Support", values: { community: false, pro: true, enterprise: true } },
{ name: "Manager dedie", category: "Support", values: { community: false, pro: false, enterprise: true } },
{ name: "SLA garanti", category: "Support", values: { community: false, pro: false, enterprise: true } },
];
// Group features by category
const categories: { name: string; features: FeatureRow[] }[] = [];
let currentCategory = "";
for (const f of resolvedFeatures) {
const cat = f.category ?? "Autre";
if (cat !== currentCategory) {
currentCategory = cat;
categories.push({ name: cat, features: [] });
}
categories[categories.length - 1].features.push(f);
}
return (
<section
style={{
padding: "var(--section-padding-y, 6rem) 0",
background: "var(--color-background)",
}}
>
<div
style={{
maxWidth: "var(--container-max-width, 1100px)",
margin: "0 auto",
padding: "0 var(--container-padding-x, 1.5rem)",
}}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, ease: EASE }}
style={{ textAlign: "center", marginBottom: "3rem" }}
>
<span style={{ fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "var(--color-accent)", display: "block", marginBottom: "0.75rem" }}>
{resolvedBadge}
</span>
<h2 style={{ fontSize: "clamp(1.75rem, 3vw, 2.5rem)", fontWeight: 700, color: "var(--color-foreground)", marginBottom: "0.75rem", letterSpacing: "-0.02em" }}>
{resolvedTitle}
</h2>
<p style={{ fontSize: "1rem", color: "var(--color-foreground-muted)", maxWidth: "500px", margin: "0 auto", lineHeight: 1.6 }}>
{resolvedSubtitle}
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1, ease: EASE }}
style={{
borderRadius: "16px",
border: "1px solid var(--color-border)",
background: "var(--color-background-card)",
overflow: "hidden",
}}
>
{/* Header row */}
<div
style={{
display: "grid",
gridTemplateColumns: `minmax(180px, 1fr) repeat(${resolvedEditions.length}, minmax(140px, 1fr))`,
borderBottom: "1px solid var(--color-border)",
}}
>
<div style={{ padding: "1.5rem" }} />
{resolvedEditions.map((edition) => (
<div
key={edition.id}
style={{
padding: "1.5rem 1rem",
textAlign: "center",
borderLeft: "1px solid var(--color-border)",
position: "relative",
background: edition.isPopular ? "color-mix(in srgb, var(--color-accent) 4%, transparent)" : "transparent",
}}
>
{edition.isPopular && (
<div style={{
position: "absolute",
top: "0.5rem",
left: "50%",
transform: "translateX(-50%)",
display: "flex",
alignItems: "center",
gap: "0.25rem",
padding: "0.125rem 0.5rem",
borderRadius: "999px",
background: "var(--color-accent)",
color: "var(--color-background)",
fontSize: "0.625rem",
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.05em",
}}>
<Crown style={{ width: 10, height: 10 }} />
Populaire
</div>
)}
<h3 style={{ fontSize: "1.125rem", fontWeight: 700, color: "var(--color-foreground)", marginBottom: "0.25rem", marginTop: edition.isPopular ? "1rem" : "0" }}>
{edition.name}
</h3>
<div>
<span style={{ fontSize: "1.5rem", fontWeight: 800, color: "var(--color-foreground)" }}>
{edition.price}
</span>
{edition.priceDetail && (
<span style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)", marginLeft: "0.25rem" }}>
{edition.priceDetail}
</span>
)}
</div>
<motion.a
href={edition.downloadUrl}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.375rem",
marginTop: "0.75rem",
padding: "0.5rem 1rem",
borderRadius: "8px",
background: edition.isPopular ? "var(--color-accent)" : "transparent",
border: edition.isPopular ? "none" : "1px solid var(--color-border)",
color: edition.isPopular ? "var(--color-background)" : "var(--color-foreground)",
fontSize: "0.8125rem",
fontWeight: 600,
textDecoration: "none",
cursor: "pointer",
}}
>
<Download style={{ width: 14, height: 14 }} />
Telecharger
</motion.a>
</div>
))}
</div>
{/* Feature rows */}
{categories.map((category) => (
<React.Fragment key={category.name}>
{/* Category header */}
<div
style={{
display: "grid",
gridTemplateColumns: `minmax(180px, 1fr) repeat(${resolvedEditions.length}, minmax(140px, 1fr))`,
background: "var(--color-background-alt)",
borderBottom: "1px solid var(--color-border)",
}}
>
<div style={{ padding: "0.625rem 1.5rem" }}>
<span style={{ fontSize: "0.6875rem", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.08em", color: "var(--color-foreground-muted)" }}>
{category.name}
</span>
</div>
{resolvedEditions.map((e) => (
<div key={e.id} style={{ borderLeft: "1px solid var(--color-border)" }} />
))}
</div>
{/* Feature rows */}
{category.features.map((feature, fi) => (
<div
key={fi}
style={{
display: "grid",
gridTemplateColumns: `minmax(180px, 1fr) repeat(${resolvedEditions.length}, minmax(140px, 1fr))`,
borderBottom: "1px solid var(--color-border)",
}}
>
<div style={{ padding: "0.75rem 1.5rem", display: "flex", alignItems: "center" }}>
<span style={{ fontSize: "0.8125rem", color: "var(--color-foreground)" }}>
{feature.name}
</span>
</div>
{resolvedEditions.map((edition) => {
const value = feature.values[edition.id];
return (
<div
key={edition.id}
style={{
padding: "0.75rem 1rem",
borderLeft: "1px solid var(--color-border)",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: edition.isPopular ? "color-mix(in srgb, var(--color-accent) 2%, transparent)" : "transparent",
}}
>
{typeof value === "boolean" ? (
value ? (
<Check style={{ width: 16, height: 16, color: "var(--color-accent)" }} />
) : (
<X style={{ width: 16, height: 16, color: "var(--color-foreground-light)" }} />
)
) : (
<span style={{ fontSize: "0.8125rem", color: "var(--color-foreground)", fontWeight: 500 }}>
{value}
</span>
)}
</div>
);
})}
</div>
))}
</React.Fragment>
))}
</motion.div>
</div>
</section>
);
}