feat: add src/components/Stats.tsx

This commit is contained in:
cupadev-admin 2026-03-08 18:33:45 +00:00
parent fea59977ef
commit a81ee8e4d0
1 changed files with 114 additions and 0 deletions

114
src/components/Stats.tsx Normal file
View File

@ -0,0 +1,114 @@
"use client";
import { useEffect, useRef, useState } from "react";
const stats = [
{
value: 280000,
suffix: "+",
label: "Anciens militaires en France",
description: "rejoignent chaque année le civil",
},
{
value: 94,
suffix: "%",
label: "Taux de placement",
description: "de nos candidats dans l'emploi",
},
{
value: 3,
suffix: " mois",
label: "Durée moyenne",
description: "pour trouver un emploi avec nous",
},
{
value: 450,
suffix: "+",
label: "Entreprises partenaires",
description: "recrutant des profils militaires",
},
];
function useCountUp(target: number, duration = 2000, start = false) {
const [count, setCount] = useState(0);
useEffect(() => {
if (!start) return;
let startTime: number | null = null;
const step = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / duration, 1);
setCount(Math.floor(progress * target));
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}, [target, duration, start]);
return count;
}
function StatCard({
value,
suffix,
label,
description,
animate,
}: (typeof stats)[0] & { animate: boolean }) {
const count = useCountUp(value, 2000, animate);
return (
<div className="text-center group">
<div className="bg-military-dark rounded-lg p-8 border border-military-green/30 hover:border-gold/50 transition-all duration-300 hover:-translate-y-1 shadow-lg">
<div className="font-serif text-5xl font-black text-gold mb-2">
{count.toLocaleString("fr-FR")}
{suffix}
</div>
<div className="text-white font-semibold text-lg mb-1">{label}</div>
<div className="text-gray-400 text-sm">{description}</div>
</div>
</div>
);
}
export default function Stats() {
const [visible, setVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ threshold: 0.3 }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<section
id="stats"
ref={ref}
className="bg-military-dark py-20 border-y border-military-green/20"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<p className="text-gold text-sm font-semibold tracking-widest uppercase mb-2">
En chiffres
</p>
<h2 className="font-serif text-3xl md:text-4xl font-bold text-white">
La reconversion militaire en France
</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat) => (
<StatCard key={stat.label} {...stat} animate={visible} />
))}
</div>
</div>
</section>
);
}