feat: Add CookiesContainer component and related logic

This commit is contained in:
Gauthier Daniels 2025-04-18 16:46:39 +02:00
parent 127d66e250
commit 78e9df72f7
4 changed files with 248 additions and 166 deletions

View File

@ -0,0 +1,25 @@
import type { PageContext } from "vike/types";
import { getTelefuncContext } from "@/lib/getTelefuncContext";
import { CookieParser } from "@/services/CookieParser";
export type ConsentCookies = keyof PageContext["cookies"]["consent"];
export async function onUpdateConsentCookie(cookieName: ConsentCookies, cookieValue: boolean) {
const context = getTelefuncContext();
const { reply } = context;
CookieParser.set(reply, cookieName, cookieValue.toString(), 365);
return { ok: true, message: "Updated consent cookie", cookieName, cookieValue };
}
export async function onAcceptAllConsentCookie() {
const context = getTelefuncContext();
const { reply } = context;
CookieParser.set(reply, "analytics", "true", 365);
CookieParser.set(reply, "customization", "true", 365);
return { ok: true, message: "Updated consents cookies" };
}

View File

@ -0,0 +1,195 @@
import { onUpdateConsentCookie, onAcceptAllConsentCookie, type ConsentCookies } from "./Cookies.telefunc";
import React, { useState, useContext, createContext } from "react";
import { usePageContext } from "vike-react/usePageContext";
import { reload } from "vike/client/router";
import { Button } from "@syntax/Button";
import { toast } from "react-toastify";
import { Toggle } from "./Toggle";
import { Link } from "./Link";
export const CookiesContext = createContext<{
cookies: {
analytics: boolean;
customization: boolean;
};
setCookie: (cookieName: ConsentCookies, cookieValue: boolean) => void;
acceptAll: () => void;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
isSelectionOpen: boolean;
setIsSelectionOpen: (isSelectionOpen: boolean) => void;
}>({
cookies: {
analytics: false,
customization: false,
},
setCookie: (_cookieName: ConsentCookies, _cookieValue: boolean) => {},
acceptAll: () => {},
isOpen: false,
setIsOpen: () => {},
isSelectionOpen: false,
setIsSelectionOpen: () => {},
});
type CookiesContainerProps = {
children?: React.ReactNode;
};
export function CookiesContainer(props: CookiesContainerProps) {
const { cookies } = usePageContext();
const [consentCookies, setConsentCookies] = useState(cookies.consent);
const [isSelectionOpen, setIsSelectionOpen] = useState(false);
const [isOpen, setIsOpen] = useState(() => {
return !Object.keys(cookies.consent).every((value) => value);
});
const handleUpdateCookie = (cookieName: ConsentCookies, cookieValue: boolean) => {
setConsentCookies((prev) => ({
...prev,
[cookieName]: cookieValue,
}));
toast
.promise(onUpdateConsentCookie(cookieName, cookieValue), {
pending: "Mise à jour des cookies...",
success: "Cookies mis à jour !",
error: "Erreur lors de la mise à jour des cookies",
})
.then(() => {
setIsOpen(false);
reload();
});
};
const handleAcceptAll = () => {
setConsentCookies({ analytics: true, customization: true });
toast
.promise(onAcceptAllConsentCookie(), {
pending: "Acceptation des cookies...",
success: "Cookies acceptés !",
error: "Erreur lors de l'acceptation des cookies",
})
.then(() => {
setIsOpen(false);
setIsSelectionOpen(false);
reload();
});
};
return (
<CookiesContext.Provider
value={{
cookies: consentCookies,
setCookie: handleUpdateCookie,
acceptAll: handleAcceptAll,
isOpen,
setIsOpen,
isSelectionOpen,
setIsSelectionOpen,
}}
>
{props.children}
{isSelectionOpen && <CookieChoices />}
{isOpen && <CookieModal />}
</CookiesContext.Provider>
);
}
function CookieChoices() {
const cookiesContext = useContext(CookiesContext);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 backdrop-blur-sm">
<div className="relative flex flex-col gap-2 bg-slate-50 dark:bg-slate-800 rounded-md shadow-xl w-full max-w-sm p-4">
<Button
variant="ghost"
size="sm"
className="absolute top-0 right-0"
onClick={() => cookiesContext.setIsSelectionOpen(false)}
>
Fermer
</Button>
<p className="font-display dark:text-slate-300 font-bold text-lg">Personnalisation des cookies 🍪</p>
<div className="flex flex-col gap-2 w-full items-start">
<Toggle
id="cookies-analytics"
label="Cookies d&lsquo;analyse (Umami et Google Analytics)"
checked={cookiesContext.cookies.analytics}
onChange={(checked) => cookiesContext.setCookie("analytics", checked)}
/>
<Toggle
id="cookies-customization"
label="Cookie de personnalisation (thème)"
checked={cookiesContext.cookies.customization}
onChange={(checked) => cookiesContext.setCookie("customization", checked)}
/>
</div>
</div>
</div>
);
}
function CookieModal() {
const cookiesContext = useContext(CookiesContext);
return (
<div className="flex flex-col fixed bottom-4 left-4 bg-slate-50 dark:bg-slate-800 z-50 rounded-md shadow-xl w-full max-w-sm overflow-hidden">
<Button
variant="ghost"
size="sm"
className="absolute top-0 right-0"
onClick={() => cookiesContext.setIsOpen(false)}
>
Fermer
</Button>
<div className="flex flex-col gap-2 p-4">
<p className="font-display dark:text-slate-300">
<span className="text-sm">Coucou c&apos;est nous...</span>
<br />
<span className="font-bold text-lg">les cookies ! 🍪</span>
</p>
<p className="text-slate-700 dark:text-slate-300">
On ne t&lsquo;embête pas longtemps, on te laisse même le choix <em>(si ça c&lsquo;est pas la classe 😎)</em>.
</p>
<p className="text-slate-700 dark:text-slate-300">
Si tu veux en savoir plus, tu peux consulter la page{" "}
<Link href="/politique-de-confidentialite" className="font-bold">
Politique de confidentialité
</Link>
.
</p>
</div>
<div className="grid items-center grid-cols-3 justify-between bg-slate-100 dark:bg-slate-700">
<button
className="cursor-pointer px-2 py-1 text-slate-600 dark:text-slate-300"
onClick={() => cookiesContext.setIsOpen(false)}
>
Non merci
</button>
<button
className="cursor-pointer px-2 py-1 text-slate-600 dark:text-slate-300"
onClick={() => cookiesContext.setIsSelectionOpen(true)}
>
Je choisis
</button>
<button
className="cursor-pointer px-2 py-1 font-bold text-white dark:text-black bg-violet-600 dark:bg-violet-300"
onClick={cookiesContext.acceptAll}
>
Oui, j&lsquo;ai faim !
</button>
</div>
</div>
);
}

View File

@ -1,16 +1,13 @@
import { onUpdateConsentCookie, onAcceptAllConsentCookie } from "./LayoutDefault.telefunc"; import { CookiesContainer } from "@/components/common/Cookies";
import { MobileNavigation } from "@syntax/MobileNavigation"; import { MobileNavigation } from "@syntax/MobileNavigation";
import { usePageContext } from "vike-react/usePageContext"; import { usePageContext } from "vike-react/usePageContext";
import { ThemeProvider } from "@/providers/ThemeProvider"; import { ThemeProvider } from "@/providers/ThemeProvider";
import { ToastContainer, toast } from "react-toastify";
import { ThemeSelector } from "@syntax/ThemeSelector"; import { ThemeSelector } from "@syntax/ThemeSelector";
import { Button } from "@/components/syntax/Button";
import { Toggle } from "@/components/common/Toggle";
import { clientOnly } from "vike-react/clientOnly"; import { clientOnly } from "vike-react/clientOnly";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { ToastContainer } from "react-toastify";
import { Navigation } from "@syntax/Navigation"; import { Navigation } from "@syntax/Navigation";
import { Link } from "@/components/common/Link"; import { Link } from "@/components/common/Link";
import { reload } from "vike/client/router";
import { Hero } from "@syntax/Hero"; import { Hero } from "@syntax/Hero";
import { Logo } from "@syntax/Logo"; import { Logo } from "@syntax/Logo";
import clsx from "clsx"; import clsx from "clsx";
@ -80,141 +77,6 @@ function Header() {
); );
} }
function CookieModal() {
const { cookies } = usePageContext();
const [consentCookies, setConsentCookies] = useState(cookies.consent);
const [isSelectionOpen, setIsSelectionOpen] = useState(false);
const [isOpen, setIsOpen] = useState(() => {
return !Object.keys(cookies.consent).every((value) => value);
});
if (isSelectionOpen) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 backdrop-blur-sm">
<div className="relative flex flex-col gap-2 bg-slate-50 dark:bg-slate-800 rounded-md shadow-xl w-full max-w-sm p-4">
<Button
variant="ghost"
size="sm"
className="absolute top-0 right-0"
onClick={() => setIsSelectionOpen(false)}
>
Fermer
</Button>
<p className="font-display dark:text-slate-300 font-bold text-lg">Personnalisation des cookies 🍪</p>
<div className="flex flex-col gap-2 w-full items-start">
<Toggle
id="cookies-analytics"
label="Cookies d&lsquo;analyse (Umami et Google Analytics)"
checked={consentCookies.analytics}
onChange={(checked) => {
setConsentCookies({ ...consentCookies, analytics: checked });
toast
.promise(onUpdateConsentCookie("analytics", checked), {
pending: "Mise à jour des cookies...",
success: "Cookies mis à jour !",
error: "Erreur lors de la mise à jour des cookies.",
})
.finally(reload);
}}
/>
<Toggle
id="cookies-customization"
label="Cookie de personnalisation (thème)"
checked={consentCookies.customization}
onChange={(checked) => {
setConsentCookies({ ...consentCookies, analytics: checked });
toast
.promise(onUpdateConsentCookie("customization", checked), {
pending: "Mise à jour des cookies...",
success: "Cookies mis à jour !",
error: "Erreur lors de la mise à jour des cookies.",
})
.then((data) => {
setConsentCookies({ ...consentCookies, [data.cookieName]: data.cookieValue });
reload();
});
}}
/>
</div>
</div>
</div>
);
}
if (!isOpen) return null;
return (
<div className="flex flex-col fixed bottom-4 left-4 bg-slate-50 dark:bg-slate-800 z-50 rounded-md shadow-xl w-full max-w-sm overflow-hidden">
<Button variant="ghost" size="sm" className="absolute top-0 right-0" onClick={() => setIsOpen(false)}>
Fermer
</Button>
<div className="flex flex-col gap-2 p-4">
<p className="font-display dark:text-slate-300">
<span className="text-sm">Coucou c&apos;est nous...</span>
<br />
<span className="font-bold text-lg">les cookies ! 🍪</span>
</p>
<p className="text-slate-700 dark:text-slate-300">
On ne t&lsquo;embête pas longtemps, on te laisse même le choix <em>(si ça c&lsquo;est pas la classe 😎)</em>.
</p>
<p className="text-slate-700 dark:text-slate-300">
Si tu veux en savoir plus, tu peux consulter la page{" "}
<Link href="/politique-de-confidentialite" className="font-bold">
Politique de confidentialité
</Link>
.
</p>
</div>
<div className="grid items-center grid-cols-3 justify-between bg-slate-100 dark:bg-slate-700">
<button
className="cursor-pointer px-2 py-1 text-slate-600 dark:text-slate-300"
onClick={() => setIsOpen(false)}
>
Non merci
</button>
<button
className="cursor-pointer px-2 py-1 text-slate-600 dark:text-slate-300"
onClick={() => setIsSelectionOpen(true)}
>
Je choisis
</button>
<button
className="cursor-pointer px-2 py-1 font-bold text-white dark:text-black bg-violet-600 dark:bg-violet-300"
onClick={() => {
setConsentCookies({ analytics: true, customization: true });
toast
.promise(onAcceptAllConsentCookie(), {
pending: "Mise à jour des cookies...",
success: "Cookies mis à jour !",
error: "Erreur lors de la mise à jour des cookies.",
})
.then(() => {
setIsOpen(false);
setIsOpen(false);
reload();
});
}}
>
Oui, j&lsquo;ai faim !
</button>
</div>
</div>
);
}
function Footer() { function Footer() {
return ( return (
<footer className="bg-slate-50 dark:bg-slate-950 text-slate-700 dark:text-slate-200"> <footer className="bg-slate-50 dark:bg-slate-950 text-slate-700 dark:text-slate-200">
@ -256,9 +118,8 @@ export default function DefaultLayout({ children }: { children: React.ReactNode
const isHomePage = urlPathname === "/"; const isHomePage = urlPathname === "/";
return ( return (
<CookiesContainer>
<ThemeProvider defaultTheme={cookies.settings.theme}> <ThemeProvider defaultTheme={cookies.settings.theme}>
<CookieModal />
<div className="flex w-full flex-col font-sans"> <div className="flex w-full flex-col font-sans">
<Header /> <Header />
@ -280,5 +141,6 @@ export default function DefaultLayout({ children }: { children: React.ReactNode
</div> </div>
<ToastContainer /> <ToastContainer />
</ThemeProvider> </ThemeProvider>
</CookiesContainer>
); );
} }

View File

@ -9,7 +9,7 @@ export function Page() {
Politique de confidentialité Politique de confidentialité
</h1> </h1>
<section> <section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">1. Introduction</h2> <h2 className="font-display text-xl text-slate-900 dark:text-slate-100">1. Introduction</h2>
<p> <p>
@ -23,7 +23,7 @@ export function Page() {
</p> </p>
</section> </section>
<section> <section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">2. Outils externes</h2> <h2 className="font-display text-xl text-slate-900 dark:text-slate-100">2. Outils externes</h2>
<p> <p>
@ -58,7 +58,7 @@ export function Page() {
</section> </section>
</section> </section>
<section> <section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">3. Cookies</h2> <h2 className="font-display text-xl text-slate-900 dark:text-slate-100">3. Cookies</h2>
<p> <p>
@ -81,7 +81,7 @@ export function Page() {
<Button <Button
variant="secondary" variant="secondary"
className="mt-2" className="w-max max-w-full"
onClick={() => { onClick={() => {
// TODO // TODO
}} }}
@ -129,7 +129,7 @@ export function Page() {
</ul> </ul>
</section> </section>
<section> <section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">4. Utilisation des données</h2> <h2 className="font-display text-xl text-slate-900 dark:text-slate-100">4. Utilisation des données</h2>
<p> <p>
@ -139,7 +139,7 @@ export function Page() {
</p> </p>
</section> </section>
<section> <section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">5. Protection des données</h2> <h2 className="font-display text-xl text-slate-900 dark:text-slate-100">5. Protection des données</h2>
<p> <p>
@ -150,7 +150,7 @@ export function Page() {
</p> </p>
</section> </section>
<section> <section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">6. Vos droits</h2> <h2 className="font-display text-xl text-slate-900 dark:text-slate-100">6. Vos droits</h2>
<p> <p>