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>
  );
}

Avis

About Counter Milestones — React About Section — Incubator