Dev #1

Merged
jessy-david-dev merged 7 commits from dev into main 2026-01-27 17:08:37 +00:00
Showing only changes of commit 78009ff3b0 - Show all commits
+294
View File
@@ -0,0 +1,294 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { Cookie, X, Check, Settings } from "lucide-react";
import Link from "next/link";
import { useState, useEffect } from "react";
type CookiePreferences = {
necessary: boolean;
analytics: boolean;
};
const COOKIE_CONSENT_KEY = "cookie-consent";
const COOKIE_PREFERENCES_KEY = "cookie-preferences";
function getInitialPreferences(): CookiePreferences {
if (typeof window === "undefined") {
return { necessary: true, analytics: false };
}
const savedPreferences = localStorage.getItem(COOKIE_PREFERENCES_KEY);
if (savedPreferences) {
try {
return JSON.parse(savedPreferences);
} catch {
return { necessary: true, analytics: false };
}
}
return { necessary: true, analytics: false };
copilot-pull-request-reviewer[bot] commented 2026-01-27 17:12:41 +00:00 (Migrated from github.com)
Review

getInitialPreferences fait JSON.parse(savedPreferences) et retourne le résultat tel quel, sans validation. Si le localStorage contient une forme inattendue (ou des types non booléens), l’état preferences peut devenir incohérent (ex: necessary manquant/false alors qu’il est supposé toujours true). Solution : valider/coercer la forme (booleans) et forcer necessary: true au chargement avant de l’utiliser.

`getInitialPreferences` fait `JSON.parse(savedPreferences)` et retourne le résultat tel quel, sans validation. Si le localStorage contient une forme inattendue (ou des types non booléens), l’état `preferences` peut devenir incohérent (ex: `necessary` manquant/false alors qu’il est supposé toujours true). Solution : valider/coercer la forme (booleans) et forcer `necessary: true` au chargement avant de l’utiliser.
}
export default function CookieBanner() {
const [showBanner, setShowBanner] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [preferences, setPreferences] = useState<CookiePreferences>(
getInitialPreferences,
);
useEffect(() => {
// Vérifier si l'utilisateur a déjà donné son consentement
const consent = localStorage.getItem(COOKIE_CONSENT_KEY);
if (!consent) {
// Délai pour ne pas afficher immédiatement au chargement
const timer = setTimeout(() => setShowBanner(true), 1000);
return () => clearTimeout(timer);
} else {
// Appliquer les préférences sauvegardées au chargement
const savedPreferences = localStorage.getItem(COOKIE_PREFERENCES_KEY);
if (savedPreferences) {
try {
const prefs = JSON.parse(savedPreferences);
if (prefs.analytics) {
// @ts-expect-error - gtag n'est pas typé
window.gtag?.("consent", "update", {
analytics_storage: "granted",
});
}
} catch {
// Ignorer les erreurs de parsing
}
}
}
}, []);
const handleAcceptAll = () => {
const newPreferences = { necessary: true, analytics: true };
saveConsent(newPreferences);
};
const handleRejectAll = () => {
const newPreferences = { necessary: true, analytics: false };
saveConsent(newPreferences);
};
const handleSavePreferences = () => {
saveConsent(preferences);
};
const saveConsent = (prefs: CookiePreferences) => {
localStorage.setItem(COOKIE_CONSENT_KEY, "true");
localStorage.setItem(COOKIE_PREFERENCES_KEY, JSON.stringify(prefs));
setPreferences(prefs);
setShowBanner(false);
setShowSettings(false);
// Activer/désactiver Google Analytics selon le consentement
if (prefs.analytics) {
enableAnalytics();
} else {
disableAnalytics();
}
};
const enableAnalytics = () => {
// Active Google Analytics via GTM Consent Mode
if (typeof window !== "undefined") {
// @ts-expect-error - gtag n'est pas typé
window.gtag?.("consent", "update", {
analytics_storage: "granted",
});
// Envoyer un événement pour GTM
// @ts-expect-error - dataLayer n'est pas typé
window.dataLayer?.push({ event: "cookie_consent_analytics" });
}
copilot-pull-request-reviewer[bot] commented 2026-01-27 17:12:42 +00:00 (Migrated from github.com)
Review

Le fichier utilise plusieurs @ts-expect-error pour window.gtag / window.dataLayer. Ici ce n’est pas une erreur ponctuelle mais une API globale attendue : il serait préférable de déclarer ces propriétés dans un fichier de types global (ex: global.d.ts avec interface Window { gtag?: ...; dataLayer?: ... }) pour éviter de masquer d’éventuelles vraies erreurs et réduire le bruit TS.

Le fichier utilise plusieurs `@ts-expect-error` pour `window.gtag` / `window.dataLayer`. Ici ce n’est pas une erreur ponctuelle mais une API globale attendue : il serait préférable de déclarer ces propriétés dans un fichier de types global (ex: `global.d.ts` avec `interface Window { gtag?: ...; dataLayer?: ... }`) pour éviter de masquer d’éventuelles vraies erreurs et réduire le bruit TS.
};
const disableAnalytics = () => {
// Désactive Google Analytics via GTM Consent Mode
if (typeof window !== "undefined") {
// @ts-expect-error - gtag n'est pas typé
window.gtag?.("consent", "update", {
analytics_storage: "denied",
});
}
};
// Fonction pour rouvrir la bannière (appelable depuis le footer)
useEffect(() => {
const handleOpenCookieSettings = () => {
setShowBanner(true);
setShowSettings(true);
};
window.addEventListener("openCookieSettings", handleOpenCookieSettings);
return () =>
window.removeEventListener(
"openCookieSettings",
handleOpenCookieSettings,
);
}, []);
return (
<AnimatePresence>
{showBanner && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
transition={{ type: "spring", damping: 25, stiffness: 200 }}
className="fixed bottom-0 left-0 right-0 z-50 p-4 md:p-6"
>
<div className="container mx-auto max-w-4xl">
<div className="bg-slate-800/95 backdrop-blur-xl border border-slate-700 rounded-2xl shadow-2xl shadow-black/20 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 md:p-6 border-b border-slate-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Cookie className="w-5 h-5 text-blue-400" />
</div>
<h3 className="text-lg font-bold text-white">
Gestion des cookies
</h3>
</div>
<button
onClick={() => setShowBanner(false)}
className="p-2 text-slate-400 hover:text-white transition-colors rounded-lg hover:bg-slate-700"
aria-label="Fermer"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-4 md:p-6">
{!showSettings ? (
<>
<p className="text-slate-300 mb-6">
Ce site utilise des cookies pour améliorer votre
expérience. Les cookies nécessaires sont essentiels au bon
fonctionnement du site. Vous pouvez choisir
d&apos;accepter ou de refuser les cookies analytiques.{" "}
<Link
href="/politique-de-confidentialite"
className="text-blue-400 hover:text-blue-300 transition-colors underline"
>
En savoir plus
</Link>
</p>
{/* Boutons */}
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={handleRejectAll}
className="flex-1 px-6 py-3 bg-slate-700 hover:bg-slate-600 text-white font-semibold rounded-xl transition-colors"
>
Refuser tout
</button>
<button
onClick={() => setShowSettings(true)}
className="flex-1 px-6 py-3 bg-slate-700 hover:bg-slate-600 text-white font-semibold rounded-xl transition-colors flex items-center justify-center gap-2"
>
<Settings className="w-4 h-4" />
Personnaliser
</button>
<button
onClick={handleAcceptAll}
className="flex-1 px-6 py-3 bg-linear-to-r from-blue-600 to-purple-600 hover:from-blue-500 hover:to-purple-500 text-white font-semibold rounded-xl transition-all"
>
Accepter tout
</button>
</div>
</>
) : (
<>
<p className="text-slate-300 mb-6">
Personnalisez vos préférences de cookies ci-dessous.
</p>
{/* Options de cookies */}
<div className="space-y-4 mb-6">
{/* Cookies nécessaires */}
<div className="flex items-center justify-between p-4 bg-slate-900/50 rounded-xl">
<div className="flex-1">
<h4 className="font-semibold text-white mb-1">
Cookies nécessaires
</h4>
<p className="text-sm text-slate-400">
Essentiels au fonctionnement du site (session,
sécurité, préférences).
</p>
</div>
<div className="ml-4">
<div className="w-12 h-6 bg-blue-600 rounded-full flex items-center justify-end px-1 cursor-not-allowed">
<div className="w-4 h-4 bg-white rounded-full flex items-center justify-center">
<Check className="w-3 h-3 text-blue-600" />
</div>
</div>
</div>
</div>
{/* Cookies analytiques */}
<div className="flex items-center justify-between p-4 bg-slate-900/50 rounded-xl">
<div className="flex-1">
<h4 className="font-semibold text-white mb-1">
Cookies analytiques
</h4>
<p className="text-sm text-slate-400">
Google Analytics pour comprendre comment vous
utilisez ce site.
</p>
</div>
<div className="ml-4">
<button
onClick={() =>
setPreferences((prev) => ({
...prev,
analytics: !prev.analytics,
}))
}
className={`w-12 h-6 rounded-full flex items-center px-1 transition-colors ${
preferences.analytics
? "bg-blue-600 justify-end"
: "bg-slate-600 justify-start"
}`}
aria-label={
preferences.analytics
? "Désactiver les cookies analytiques"
: "Activer les cookies analytiques"
}
>
<div className="w-4 h-4 bg-white rounded-full flex items-center justify-center">
{preferences.analytics && (
<Check className="w-3 h-3 text-blue-600" />
)}
</div>
</button>
</div>
</div>
</div>
{/* Boutons */}
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={() => setShowSettings(false)}
className="flex-1 px-6 py-3 bg-slate-700 hover:bg-slate-600 text-white font-semibold rounded-xl transition-colors"
>
Retour
</button>
<button
onClick={handleSavePreferences}
className="flex-1 px-6 py-3 bg-linear-to-r from-blue-600 to-purple-600 hover:from-blue-500 hover:to-purple-500 text-white font-semibold rounded-xl transition-all"
>
Sauvegarder mes choix
</button>
</div>
</>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}