Retour au catalogue

Waitlist Countdown Combo

Inscription waitlist combinee avec un compte a rebours vers la date de lancement. Timer anime et formulaire email.

waitlistmedium Both Responsive a11y
bolddarksaasuniversalagencycentered
Theme
"use client";

import { useRef, useState, useEffect } from "react";
import { motion, useInView } from "framer-motion";
import { Clock, ArrowRight, Sparkles } from "lucide-react";

interface TimerLabels {
  days: string;
  hours: string;
  minutes: string;
  seconds: string;
}

interface WaitlistCountdownComboProps {
  title?: string;
  subtitle?: string;
  ctaLabel?: string;
  placeholder?: string;
  badge?: string;
  targetDate?: string;
  timerLabels?: TimerLabels;
  note?: string;
}

const EASE: [number, number, number, number] = [0.16, 1, 0.3, 1];

function useCountdown(target: string) {
  const [time, setTime] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
  useEffect(() => {
    const calc = () => {
      const diff = Math.max(0, new Date(target).getTime() - Date.now());
      setTime({
        days: Math.floor(diff / 86400000),
        hours: Math.floor((diff % 86400000) / 3600000),
        minutes: Math.floor((diff % 3600000) / 60000),
        seconds: Math.floor((diff % 60000) / 1000),
      });
    };
    calc();
    const id = setInterval(calc, 1000);
    return () => clearInterval(id);
  }, [target]);
  return time;
}

export default function WaitlistCountdownCombo({
  title = "Le lancement approche",
  subtitle = "Reservez votre place avant l'ouverture officielle.",
  ctaLabel = "Me prevenir",
  placeholder = "votre@email.com",
  badge = "Lancement imminent",
  targetDate = "2026-06-15T00:00:00",
  timerLabels = { days: "Jours", hours: "Heures", minutes: "Minutes", seconds: "Secondes" },
  note,
}: WaitlistCountdownComboProps) {
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, margin: "-80px" });
  const time = useCountdown(targetDate);
  const units: (keyof typeof time)[] = ["days", "hours", "minutes", "seconds"];

  return (
    <section style={{ padding: "var(--section-padding-y) 0", background: "var(--color-background)", position: "relative", overflow: "hidden" }}>
      <div ref={ref} style={{ maxWidth: 640, margin: "0 auto", padding: "0 var(--container-padding-x)", textAlign: "center", position: "relative", zIndex: 1 }}>
        <motion.div initial={{ opacity: 0, y: 20 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.5, ease: EASE }}>
          {badge && (
            <span style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "0.4rem 1rem", borderRadius: "var(--radius-full)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", fontSize: "0.8125rem", fontWeight: 500, color: "var(--color-foreground-muted)", marginBottom: "1.25rem" }}>
              <Clock style={{ width: 14, height: 14, color: "var(--color-accent)" }} />
              {badge}
            </span>
          )}
          <h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.5rem, 3vw, 2.5rem)", fontWeight: 700, lineHeight: 1.15, letterSpacing: "-0.02em", color: "var(--color-foreground)", marginBottom: "0.75rem" }}>{title}</h2>
          <p style={{ fontSize: "1rem", lineHeight: 1.65, color: "var(--color-foreground-muted)", marginBottom: "2.5rem" }}>{subtitle}</p>
        </motion.div>

        {/* Countdown timer */}
        <motion.div initial={{ opacity: 0, y: 16 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, delay: 0.1, ease: EASE }} style={{ display: "flex", justifyContent: "center", gap: "1rem", marginBottom: "2.5rem", flexWrap: "wrap" }}>
          {units.map((unit, i) => (
            <motion.div key={unit} initial={{ opacity: 0, scale: 0.9 }} animate={inView ? { opacity: 1, scale: 1 } : {}} transition={{ duration: 0.4, delay: 0.15 + i * 0.06, ease: EASE }} style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "1.25rem 1.5rem", borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", minWidth: 90 }}>
              <span style={{ fontSize: "2rem", fontWeight: 800, fontFamily: "var(--font-sans)", color: "var(--color-foreground)", lineHeight: 1, fontVariantNumeric: "tabular-nums" }}>{String(time[unit]).padStart(2, "0")}</span>
              <span style={{ fontSize: "0.6875rem", fontWeight: 500, textTransform: "uppercase", letterSpacing: "0.1em", color: "var(--color-foreground-muted)", marginTop: "0.375rem" }}>{timerLabels[unit]}</span>
            </motion.div>
          ))}
        </motion.div>

        {/* Email form */}
        <motion.div initial={{ opacity: 0, y: 12 }} animate={inView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.5, delay: 0.25, ease: EASE }} style={{ display: "flex", gap: 8, maxWidth: 440, margin: "0 auto" }}>
          <input type="email" placeholder={placeholder} style={{ flex: 1, padding: "0.875rem 1.25rem", borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", color: "var(--color-foreground)", fontSize: "0.9375rem", outline: "none", minWidth: 0 }} />
          <button style={{ padding: "0.875rem 1.5rem", borderRadius: "var(--radius-md)", background: "var(--color-accent)", color: "var(--color-foreground)", fontWeight: 600, fontSize: "0.9375rem", border: "none", cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}>
            {ctaLabel} <ArrowRight style={{ width: 16, height: 16 }} />
          </button>
        </motion.div>

        {note && (
          <motion.p initial={{ opacity: 0 }} animate={inView ? { opacity: 1 } : {}} transition={{ duration: 0.4, delay: 0.35 }} style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)", marginTop: "1.25rem", display: "flex", alignItems: "center", justifyContent: "center", gap: 6 }}>
            <Sparkles style={{ width: 13, height: 13, color: "var(--color-accent)" }} />
            {note}
          </motion.p>
        )}
      </div>
    </section>
  );
}

Avis

Waitlist Countdown Combo — React Waitlist Section — Incubator