Retour au catalogue

Early Access

Waitlist d'acces anticipe avec tiers de privileges, compteur d'inscrits anime et avantages exclusifs.

waitlistmedium Both Responsive a11y
elegantboldsaasecommercecentered
Theme
"use client";

import { useRef, useState } from "react";
import { motion, useInView, AnimatePresence } from "framer-motion";
import { Crown, ArrowRight, Check, Sparkles, Users, Star, Zap } from "lucide-react";

interface Tier {
  name: string;
  range: string;
  perks: string[];
  active: boolean;
}

interface SocialPerson {
  initials: string;
  name: string;
}

interface WaitlistEarlyAccessProps {
  title?: string;
  subtitle?: string;
  placeholder?: string;
  ctaLabel?: string;
  badge?: string;
  currentSignups?: number;
  maxSignups?: number;
  tiers?: Tier[];
  socialProof?: SocialPerson[];
}

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

export default function WaitlistEarlyAccess({
  title = "Acces anticipe exclusif",
  subtitle = "Soyez parmi les premiers.",
  placeholder = "votre@email.com",
  ctaLabel = "Obtenir mon acces",
  badge = "Places limitees",
  currentSignups = 2847,
  maxSignups = 5000,
  tiers = [],
  socialProof = [],
}: WaitlistEarlyAccessProps) {
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, margin: "-80px" });
  const [submitted, setSubmitted] = useState(false);

  const fillPercent = maxSignups > 0 ? (currentSignups / maxSignups) * 100 : 0;

  return (
    <section
      style={{
        padding: "5rem 0",
        background: "var(--color-background)",
        position: "relative",
        overflow: "hidden",
      }}
    >
      {/* Ambient glow */}
      <div
        aria-hidden
        style={{
          position: "absolute",
          top: "10%",
          left: "50%",
          transform: "translateX(-50%)",
          width: 600,
          height: 600,
          borderRadius: "50%",
          background: "var(--color-accent)",
          opacity: 0.03,
          filter: "blur(120px)",
          pointerEvents: "none",
        }}
      />

      <div ref={ref} style={{ maxWidth: 640, margin: "0 auto", padding: "0 1.5rem", position: "relative", zIndex: 1 }}>
        {/* Header */}
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={inView ? { opacity: 1, y: 0 } : {}}
          transition={{ duration: 0.6, ease: EASE }}
          style={{ textAlign: "center", marginBottom: "2.5rem" }}
        >
          <span
            style={{
              display: "inline-flex",
              alignItems: "center",
              gap: 6,
              padding: "0.375rem 0.875rem",
              borderRadius: "var(--radius-full)",
              background: "var(--color-accent)",
              color: "var(--color-background)",
              fontSize: "0.75rem",
              fontWeight: 700,
              textTransform: "uppercase",
              letterSpacing: "0.05em",
              marginBottom: "1.25rem",
            }}
          >
            <Crown style={{ width: 12, height: 12 }} />
            {badge}
          </span>
          <h2
            style={{
              fontSize: "clamp(1.5rem, 3.5vw, 2.5rem)",
              fontWeight: 800,
              color: "var(--color-foreground)",
              letterSpacing: "-0.025em",
              lineHeight: 1.15,
              marginBottom: "0.75rem",
            }}
          >
            {title}
          </h2>
          <p style={{ fontSize: "1rem", color: "var(--color-foreground-muted)", lineHeight: 1.65, maxWidth: 480, margin: "0 auto" }}>
            {subtitle}
          </p>
        </motion.div>

        {/* Progress counter */}
        <motion.div
          initial={{ opacity: 0, y: 12 }}
          animate={inView ? { opacity: 1, y: 0 } : {}}
          transition={{ duration: 0.5, delay: 0.1, ease: EASE }}
          style={{
            padding: "1.25rem 1.5rem",
            borderRadius: "var(--radius-lg)",
            border: "1px solid var(--color-border)",
            background: "var(--color-background-card)",
            marginBottom: "1.5rem",
          }}
        >
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
              <Users style={{ width: 14, height: 14, color: "var(--color-accent)" }} />
              <span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-foreground)" }}>
                {currentSignups.toLocaleString("fr-FR")} inscrits
              </span>
            </div>
            <span style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)" }}>
              {maxSignups.toLocaleString("fr-FR")} places
            </span>
          </div>
          <div style={{ height: 6, borderRadius: 3, background: "var(--color-background-alt)", overflow: "hidden" }}>
            <motion.div
              initial={{ width: 0 }}
              animate={inView ? { width: `${fillPercent}%` } : {}}
              transition={{ duration: 1.2, delay: 0.3, ease: EASE }}
              style={{ height: "100%", borderRadius: 3, background: "var(--color-accent)" }}
            />
          </div>
          <p style={{ fontSize: "0.75rem", color: "var(--color-foreground-light)", marginTop: 6, textAlign: "right" }}>
            {Math.round(fillPercent)}% rempli
          </p>
        </motion.div>

        {/* Form or confirmation */}
        <AnimatePresence mode="wait">
          {!submitted ? (
            <motion.div
              key="form"
              initial={{ opacity: 0, y: 12 }}
              animate={inView ? { opacity: 1, y: 0 } : {}}
              exit={{ opacity: 0, y: -12 }}
              transition={{ duration: 0.5, delay: 0.15, ease: EASE }}
              style={{ marginBottom: "2rem" }}
            >
              <div style={{ display: "flex", gap: 8 }}>
                <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
                  onClick={() => setSubmitted(true)}
                  style={{
                    padding: "0.875rem 1.5rem",
                    borderRadius: "var(--radius-md)",
                    background: "var(--color-accent)",
                    color: "var(--color-background)",
                    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>
              </div>

              {/* Social proof */}
              {socialProof.length > 0 && (
                <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 8, marginTop: "1rem" }}>
                  <div style={{ display: "flex" }}>
                    {socialProof.map((person, i) => (
                      <div
                        key={person.initials}
                        style={{
                          width: 28,
                          height: 28,
                          borderRadius: "50%",
                          background: "var(--color-accent-subtle)",
                          border: "2px solid var(--color-background)",
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "center",
                          fontSize: "0.5625rem",
                          fontWeight: 700,
                          color: "var(--color-accent)",
                          marginLeft: i > 0 ? -8 : 0,
                        }}
                      >
                        {person.initials}
                      </div>
                    ))}
                  </div>
                  <span style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)" }}>
                    +{currentSignups.toLocaleString("fr-FR")} inscrits
                  </span>
                </div>
              )}
            </motion.div>
          ) : (
            <motion.div
              key="confirmed"
              initial={{ opacity: 0, scale: 0.95 }}
              animate={{ opacity: 1, scale: 1 }}
              transition={{ duration: 0.5, ease: EASE }}
              style={{
                padding: "2rem",
                borderRadius: "var(--radius-lg)",
                border: "1px solid var(--color-accent)",
                background: "var(--color-accent-subtle)",
                textAlign: "center",
                marginBottom: "2rem",
              }}
            >
              <Sparkles style={{ width: 28, height: 28, color: "var(--color-accent)", marginBottom: 12 }} />
              <h3 style={{ fontSize: "1.125rem", fontWeight: 700, color: "var(--color-foreground)", marginBottom: "0.375rem" }}>
                Vous etes inscrit !
              </h3>
              <p style={{ fontSize: "0.875rem", color: "var(--color-foreground-muted)" }}>
                Position #{currentSignups + 1} — verifiez votre email pour confirmer.
              </p>
            </motion.div>
          )}
        </AnimatePresence>

        {/* Tiers */}
        {tiers.length > 0 && (
          <motion.div
            initial={{ opacity: 0, y: 20 }}
            animate={inView ? { opacity: 1, y: 0 } : {}}
            transition={{ duration: 0.5, delay: 0.25, ease: EASE }}
          >
            <p
              style={{
                fontSize: "0.75rem",
                fontWeight: 600,
                textTransform: "uppercase",
                letterSpacing: "0.1em",
                color: "var(--color-foreground-muted)",
                textAlign: "center",
                marginBottom: "1rem",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                gap: 6,
              }}
            >
              <Star style={{ width: 12, height: 12, color: "var(--color-accent)" }} />
              Paliers d'avantages
            </p>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))", gap: 12 }}>
              {tiers.map((tier, i) => (
                <motion.div
                  key={tier.name}
                  initial={{ opacity: 0, y: 16 }}
                  animate={inView ? { opacity: 1, y: 0 } : {}}
                  transition={{ duration: 0.4, delay: 0.3 + i * 0.1, ease: EASE }}
                  style={{
                    padding: "1.25rem",
                    borderRadius: "var(--radius-lg)",
                    border: tier.active ? "2px solid var(--color-accent)" : "1px solid var(--color-border)",
                    background: tier.active ? "var(--color-accent-subtle)" : "var(--color-background-card)",
                    position: "relative",
                  }}
                >
                  {tier.active && (
                    <span
                      style={{
                        position: "absolute",
                        top: -8,
                        left: "50%",
                        transform: "translateX(-50%)",
                        padding: "0.125rem 0.625rem",
                        borderRadius: "var(--radius-full)",
                        background: "var(--color-accent)",
                        color: "var(--color-background)",
                        fontSize: "0.625rem",
                        fontWeight: 700,
                        textTransform: "uppercase",
                        letterSpacing: "0.05em",
                        whiteSpace: "nowrap",
                      }}
                    >
                      Votre tier
                    </span>
                  )}
                  <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: "0.375rem" }}>
                    <Zap style={{ width: 14, height: 14, color: tier.active ? "var(--color-accent)" : "var(--color-foreground-light)" }} />
                    <h4 style={{ fontSize: "0.875rem", fontWeight: 700, color: "var(--color-foreground)" }}>{tier.name}</h4>
                  </div>
                  <p style={{ fontSize: "0.6875rem", color: "var(--color-foreground-light)", marginBottom: "0.75rem" }}>
                    Inscrits {tier.range}
                  </p>
                  <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
                    {tier.perks.map((perk) => (
                      <li
                        key={perk}
                        style={{
                          display: "flex",
                          alignItems: "center",
                          gap: 6,
                          fontSize: "0.75rem",
                          color: "var(--color-foreground-muted)",
                          marginBottom: 4,
                        }}
                      >
                        <Check style={{ width: 12, height: 12, color: "var(--color-accent)", flexShrink: 0 }} />
                        {perk}
                      </li>
                    ))}
                  </ul>
                </motion.div>
              ))}
            </div>
          </motion.div>
        )}
      </div>
    </section>
  );
}

Avis

Early Access — React Waitlist Section — Incubator