feat/analytics #9
25
app/components/common/Cookies.telefunc.ts
Normal file
25
app/components/common/Cookies.telefunc.ts
Normal 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" };
|
||||||
|
}
|
||||||
195
app/components/common/Cookies.tsx
Normal file
195
app/components/common/Cookies.tsx
Normal 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‘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'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‘embête pas longtemps, on te laisse même le choix <em>(si ça c‘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‘ai faim !
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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‘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'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‘embête pas longtemps, on te laisse même le choix <em>(si ça c‘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‘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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user