Retour au catalogue
About Counter Milestones
Section about avec compteurs animes et timeline de jalons connectes.
aboutmedium Both Responsive a11y
corporateboldsaasagencyuniversalstacked
Theme
"use client";
import React, { useEffect, useState, useRef } from "react";
import { motion, useInView } from "framer-motion";
interface Counter {
value: number;
suffix?: string;
label: string;
}
interface Milestone {
year: string;
title: string;
description: string;
}
interface AboutCounterMilestonesProps {
badge?: string;
title?: string;
subtitle?: string;
counters?: Counter[];
milestones?: Milestone[];
}
function AnimatedCounter({ value, suffix = "", label }: Counter) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true });
const [display, setDisplay] = useState(0);
useEffect(() => {
if (!isInView) return;
let start = 0;
const step = Math.max(1, Math.floor(value / 60));
const timer = setInterval(() => {
start += step;
if (start >= value) { setDisplay(value); clearInterval(timer); }
else setDisplay(start);
}, 20);
return () => clearInterval(timer);
}, [isInView, value]);
return (
<div ref={ref} className="text-center">
<p className="text-4xl md:text-5xl font-bold tabular-nums" style={{ color: "var(--color-accent)" }}>
{display}{suffix}
</p>
<p className="mt-2 text-sm" style={{ color: "var(--color-foreground-muted)" }}>{label}</p>
</div>
);
}
export default function AboutCounterMilestones({ badge, title, subtitle, counters = [], milestones = [] }: AboutCounterMilestonesProps) {
return (
<section className="py-[var(--section-padding-y,6rem)]" style={{ backgroundColor: "var(--color-background)" }}>
<div className="mx-auto max-w-6xl px-[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 }} className="text-center max-w-2xl mx-auto mb-16">
{badge && <span className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border" style={{ color: "var(--color-accent)", borderColor: "var(--color-border)" }}>{badge}</span>}
{title && <h2 className="mt-4 text-3xl font-bold tracking-tight md:text-4xl" style={{ color: "var(--color-foreground)" }}>{title}</h2>}
{subtitle && <p className="mt-3 text-sm" style={{ color: "var(--color-foreground-muted)" }}>{subtitle}</p>}
</motion.div>
{/* Counters */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-20">
{counters.map((counter, i) => <AnimatedCounter key={i} {...counter} />)}
</div>
{/* Timeline */}
<div className="relative">
<div className="absolute left-4 md:left-1/2 top-0 bottom-0 w-px" style={{ backgroundColor: "var(--color-border)" }} />
<div className="space-y-12">
{milestones.map((ms, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ delay: i * 0.1, duration: 0.5 }}
className={`relative grid grid-cols-1 md:grid-cols-2 gap-8 ${i % 2 === 0 ? "" : "md:direction-rtl"}`}
>
<div className={`${i % 2 === 0 ? "md:text-right md:pr-12" : "md:order-2 md:pl-12"}`}>
<span className="text-xs font-bold tracking-wider" style={{ color: "var(--color-accent)" }}>{ms.year}</span>
<h3 className="mt-1 text-lg font-semibold" style={{ color: "var(--color-foreground)" }}>{ms.title}</h3>
<p className="mt-2 text-sm leading-relaxed" style={{ color: "var(--color-foreground-muted)" }}>{ms.description}</p>
</div>
<div className={`hidden md:block ${i % 2 === 0 ? "" : "md:order-1"}`} />
<div className="absolute left-4 md:left-1/2 top-1 w-3 h-3 rounded-full -translate-x-1/2 border-2" style={{ backgroundColor: "var(--color-background)", borderColor: "var(--color-accent)" }} />
</motion.div>
))}
</div>
</div>
</div>
</section>
);
}