Retour au catalogue

Cookie Consent

Banniere RGPD de consentement aux cookies avec options granulaires et animation slide-up.

bannersmedium Both Responsive a11y
minimalcorporateuniversalstacked
Theme
"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Cookie, Shield, ChevronDown, ChevronUp, X } from "lucide-react";

interface CookieCategory {
  id: string;
  label: string;
  description: string;
  required: boolean;
}

interface BannerCookieConsentProps {
  title?: string;
  description?: string;
  acceptLabel?: string;
  rejectLabel?: string;
  customizeLabel?: string;
  privacyLink?: string;
  privacyLabel?: string;
  categories?: CookieCategory[];
}

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

export default function BannerCookieConsent({
  title = "Nous respectons votre vie privee",
  description = "Ce site utilise des cookies pour ameliorer votre experience.",
  acceptLabel = "Tout accepter",
  rejectLabel = "Tout refuser",
  customizeLabel = "Personnaliser",
  privacyLink = "#",
  privacyLabel = "Politique de confidentialite",
  categories = [],
}: BannerCookieConsentProps) {
  const [visible, setVisible] = useState(true);
  const [expanded, setExpanded] = useState(false);
  const [toggles, setToggles] = useState<Record<string, boolean>>(() => {
    const initial: Record<string, boolean> = {};
    categories.forEach((c) => {
      initial[c.id] = c.required;
    });
    return initial;
  });

  const handleToggle = (id: string, required: boolean) => {
    if (required) return;
    setToggles((prev) => ({ ...prev, [id]: !prev[id] }));
  };

  if (!visible) return null;

  return (
    <AnimatePresence>
      {visible && (
        <motion.div
          initial={{ y: 100, opacity: 0 }}
          animate={{ y: 0, opacity: 1 }}
          exit={{ y: 100, opacity: 0 }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{
            position: "fixed",
            bottom: 24,
            left: "50%",
            transform: "translateX(-50%)",
            zIndex: 9999,
            width: "calc(100% - 2rem)",
            maxWidth: 540,
          }}
        >
          <div
            style={{
              background: "var(--color-background-card)",
              border: "1px solid var(--color-border)",
              borderRadius: "var(--radius-xl)",
              padding: "1.5rem",
              boxShadow: "0 25px 50px -12px rgba(0,0,0,0.25)",
              backdropFilter: "blur(16px)",
            }}
          >
            {/* Header */}
            <div style={{ display: "flex", alignItems: "flex-start", gap: 12, marginBottom: "1rem" }}>
              <div
                style={{
                  width: 40,
                  height: 40,
                  borderRadius: "var(--radius-lg)",
                  background: "var(--color-accent-subtle)",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  flexShrink: 0,
                }}
              >
                <Cookie style={{ width: 20, height: 20, color: "var(--color-accent)" }} />
              </div>
              <div style={{ flex: 1 }}>
                <h3
                  style={{
                    fontSize: "0.9375rem",
                    fontWeight: 600,
                    color: "var(--color-foreground)",
                    marginBottom: 4,
                  }}
                >
                  {title}
                </h3>
                <p style={{ fontSize: "0.8125rem", lineHeight: 1.5, color: "var(--color-foreground-muted)" }}>
                  {description}
                </p>
              </div>
              <button
                onClick={() => setVisible(false)}
                style={{
                  background: "none",
                  border: "none",
                  cursor: "pointer",
                  color: "var(--color-foreground-light)",
                  padding: 4,
                  flexShrink: 0,
                }}
                aria-label="Fermer"
              >
                <X style={{ width: 16, height: 16 }} />
              </button>
            </div>

            {/* Expandable categories */}
            <AnimatePresence>
              {expanded && categories.length > 0 && (
                <motion.div
                  initial={{ height: 0, opacity: 0 }}
                  animate={{ height: "auto", opacity: 1 }}
                  exit={{ height: 0, opacity: 0 }}
                  transition={{ duration: 0.3, ease: EASE }}
                  style={{ overflow: "hidden", marginBottom: "1rem" }}
                >
                  <div
                    style={{
                      borderRadius: "var(--radius-md)",
                      border: "1px solid var(--color-border)",
                      overflow: "hidden",
                    }}
                  >
                    {categories.map((cat, i) => (
                      <div
                        key={cat.id}
                        style={{
                          display: "flex",
                          alignItems: "center",
                          justifyContent: "space-between",
                          padding: "0.75rem 1rem",
                          borderBottom: i < categories.length - 1 ? "1px solid var(--color-border)" : "none",
                          background: "var(--color-background-alt)",
                        }}
                      >
                        <div>
                          <p style={{ fontSize: "0.8125rem", fontWeight: 500, color: "var(--color-foreground)" }}>
                            <Shield
                              style={{
                                width: 12,
                                height: 12,
                                display: "inline",
                                marginRight: 6,
                                color: "var(--color-accent)",
                                verticalAlign: "middle",
                              }}
                            />
                            {cat.label}
                            {cat.required && (
                              <span style={{ fontSize: "0.6875rem", color: "var(--color-foreground-light)", marginLeft: 6 }}>
                                (requis)
                              </span>
                            )}
                          </p>
                          <p style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)", marginTop: 2 }}>
                            {cat.description}
                          </p>
                        </div>
                        <button
                          onClick={() => handleToggle(cat.id, cat.required)}
                          style={{
                            width: 40,
                            height: 22,
                            borderRadius: 11,
                            border: "none",
                            cursor: cat.required ? "not-allowed" : "pointer",
                            background: toggles[cat.id] ? "var(--color-accent)" : "var(--color-border)",
                            position: "relative",
                            transition: "background 0.2s",
                            flexShrink: 0,
                            opacity: cat.required ? 0.6 : 1,
                          }}
                        >
                          <motion.div
                            animate={{ x: toggles[cat.id] ? 20 : 2 }}
                            transition={{ duration: 0.2, ease: EASE }}
                            style={{
                              width: 18,
                              height: 18,
                              borderRadius: 9,
                              background: "var(--color-background)",
                              position: "absolute",
                              top: 2,
                              left: 0,
                              boxShadow: "0 1px 3px rgba(0,0,0,0.15)",
                            }}
                          />
                        </button>
                      </div>
                    ))}
                  </div>
                </motion.div>
              )}
            </AnimatePresence>

            {/* Actions */}
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
              <button
                onClick={() => setVisible(false)}
                style={{
                  flex: 1,
                  padding: "0.75rem 1rem",
                  borderRadius: "var(--radius-md)",
                  background: "var(--color-accent)",
                  color: "var(--color-background)",
                  fontWeight: 600,
                  fontSize: "0.8125rem",
                  border: "none",
                  cursor: "pointer",
                  minWidth: 100,
                }}
              >
                {acceptLabel}
              </button>
              <button
                onClick={() => setVisible(false)}
                style={{
                  flex: 1,
                  padding: "0.75rem 1rem",
                  borderRadius: "var(--radius-md)",
                  background: "var(--color-background-alt)",
                  color: "var(--color-foreground)",
                  fontWeight: 500,
                  fontSize: "0.8125rem",
                  border: "1px solid var(--color-border)",
                  cursor: "pointer",
                  minWidth: 100,
                }}
              >
                {rejectLabel}
              </button>
              <button
                onClick={() => setExpanded(!expanded)}
                style={{
                  flex: 1,
                  padding: "0.75rem 1rem",
                  borderRadius: "var(--radius-md)",
                  background: "transparent",
                  color: "var(--color-foreground-muted)",
                  fontWeight: 500,
                  fontSize: "0.8125rem",
                  border: "1px solid var(--color-border)",
                  cursor: "pointer",
                  display: "inline-flex",
                  alignItems: "center",
                  justifyContent: "center",
                  gap: 4,
                  minWidth: 100,
                }}
              >
                {customizeLabel}
                {expanded ? <ChevronUp style={{ width: 14, height: 14 }} /> : <ChevronDown style={{ width: 14, height: 14 }} />}
              </button>
            </div>

            {/* Privacy link */}
            <div style={{ textAlign: "center", marginTop: "0.75rem" }}>
              <a
                href={privacyLink}
                style={{
                  fontSize: "0.75rem",
                  color: "var(--color-foreground-light)",
                  textDecoration: "underline",
                  textUnderlineOffset: 2,
                }}
              >
                {privacyLabel}
              </a>
            </div>
          </div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Avis

Cookie Consent — React Banners Section — Incubator