feat: add src/components/Stats.tsx
This commit is contained in:
parent
fea59977ef
commit
a81ee8e4d0
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue