Merge pull request 'feat/analytics' (#9) from feat/analytics into main
Some checks failed
Update Memento Dev on VPS / deploy (push) Has been cancelled

Reviewed-on: #9
This commit is contained in:
Gauthier Daniels 2025-04-18 15:05:50 +00:00
commit c0b4c771d4
21 changed files with 801 additions and 37 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 onSetAllConsentCookie(cookieValue: boolean) {
const context = getTelefuncContext();
const { reply } = context;
CookieParser.set(reply, "analytics", cookieValue.toString(), 365);
CookieParser.set(reply, "customization", cookieValue.toString(), 365);
return { ok: true, message: "Updated consents cookies" };
}

View File

@ -0,0 +1,192 @@
import { onUpdateConsentCookie, onSetAllConsentCookie, type ConsentCookies } from "./Cookies.telefunc";
import React, { useState, useContext, createContext, useMemo } 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;
setAllCookies: (cookieValue: boolean) => void;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
isSelectionOpen: boolean;
setIsSelectionOpen: (isSelectionOpen: boolean) => void;
}>({
cookies: {
analytics: false,
customization: false,
},
setCookie: (_cookieName: ConsentCookies, _cookieValue: boolean) => {},
setAllCookies: () => {},
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 toastPromiseMessages = useMemo(
() => ({
pending: "Mise à jour des cookies...",
success: "Cookies mis à jour !",
error: "Erreur lors de la mise à jour des cookies",
}),
[],
);
const handleUpdateCookie = (cookieName: ConsentCookies, cookieValue: boolean) => {
setConsentCookies((prev) => ({
...prev,
[cookieName]: cookieValue,
}));
toast.promise(onUpdateConsentCookie(cookieName, cookieValue), toastPromiseMessages).then(() => {
setIsOpen(false);
reload();
});
};
const handleSetAll = (value: boolean) => {
setConsentCookies({ analytics: true, customization: true });
toast.promise(onSetAllConsentCookie(value), toastPromiseMessages).then(() => {
setIsOpen(false);
setIsSelectionOpen(false);
reload();
});
};
return (
<CookiesContext.Provider
value={{
cookies: consentCookies,
setCookie: handleUpdateCookie,
setAllCookies: handleSetAll,
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.setAllCookies(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.setAllCookies(true)}
>
Oui, j&lsquo;ai faim !
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
import React from "react";
import clsx from "clsx";
type ToggleProps = {
id: string;
label: string;
onChange?: (checked: boolean) => void;
checked: boolean;
};
export function Toggle(props: ToggleProps) {
return (
<div className="flex items-center justify-center">
<input
type="checkbox"
id={props.id}
className="sr-only"
onChange={(e) => props.onChange?.(e.target.checked)}
checked={props.checked}
aria-checked={props.checked}
role="switch"
aria-label={props.label}
/>
<label htmlFor={props.id} className="flex cursor-pointer items-center justify-between rounded-full">
<span className="relative flex h-6 w-10 items-center">
<span
className={clsx(
"h-4 w-4 rounded-full bg-white shadow-md transition-transform duration-200 ease-in-out z-10",
props.checked ? "translate-x-[calc(100%+.25em)]" : "translate-x-1",
)}
/>
<span
className={clsx(
"absolute top-1/2 left-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 rounded-full transition duration-200 ease-in-out z-0",
props.checked ? "bg-violet-500" : "bg-slate-300",
)}
/>
</span>
<span className="ml-2 text-sm text-slate-700 dark:text-slate-300">{props.label}</span>
</label>
</div>
);
}

View File

@ -7,6 +7,8 @@ const variantStyles = {
"bg-violet-300 font-semibold text-slate-900 hover:bg-violet-200 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-300/50 active:bg-violet-500", "bg-violet-300 font-semibold text-slate-900 hover:bg-violet-200 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-300/50 active:bg-violet-500",
secondary: secondary:
"bg-slate-800 font-medium text-white hover:bg-slate-700 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50 active:text-slate-400", "bg-slate-800 font-medium text-white hover:bg-slate-700 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50 active:text-slate-400",
ghost:
"bg-transparent font-medium text-slate-900 dark:text-slate-400 hover:bg-slate-100 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-300/50 active:bg-slate-200",
}; };
const sizeStyles = { const sizeStyles = {

View File

@ -161,6 +161,7 @@ export function Navigation({
onLinkClick?: React.MouseEventHandler<HTMLAnchorElement>; onLinkClick?: React.MouseEventHandler<HTMLAnchorElement>;
}) { }) {
const firstSections = navigation.filter((section) => section.position === "start"); const firstSections = navigation.filter((section) => section.position === "start");
const lastSections = navigation.filter((section) => section.position === "end");
const filteredSections = navigation const filteredSections = navigation
.filter((section) => section.position === "auto" || section.position === undefined) .filter((section) => section.position === "auto" || section.position === undefined)
@ -192,6 +193,12 @@ export function Navigation({
))} ))}
</li> </li>
))} ))}
<li>
<h2 className="font-display font-bold text-base text-slate-900 dark:text-white">{lastSections[0]?.type}</h2>
{lastSections.map((section) => (
<NavigationItem key={section.title} section={section} onLinkClick={onLinkClick} />
))}
</li>
</ul> </ul>
</nav> </nav>
); );

View File

@ -48,7 +48,7 @@ export function ThemeSelector(props: React.ComponentPropsWithoutRef<typeof Listb
<Listbox as="div" value={theme} onChange={setTheme} {...props}> <Listbox as="div" value={theme} onChange={setTheme} {...props}>
<Label className="sr-only">Theme</Label> <Label className="sr-only">Theme</Label>
<ListboxButton <ListboxButton
className="flex h-6 w-6 items-center justify-center rounded-lg ring-1 shadow-md shadow-black/5 ring-black/5 dark:bg-slate-700 dark:ring-white/5 dark:ring-inset" className="flex h-6 w-6 items-center justify-center rounded-lg ring-1 shadow-md shadow-black/5 ring-black/5 dark:bg-slate-700 dark:ring-white/5 dark:ring-inset cursor-pointer"
aria-label="Theme" aria-label="Theme"
> >
<LightIcon className={clsx("h-4 w-4 dark:hidden", "fill-violet-400")} /> <LightIcon className={clsx("h-4 w-4 dark:hidden", "fill-violet-400")} />

View File

@ -36,6 +36,7 @@ export default tseslint.config(
"@typescript-eslint/no-unused-vars": [1, { argsIgnorePattern: "^_" }], "@typescript-eslint/no-unused-vars": [1, { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-namespace": 0, "@typescript-eslint/no-namespace": 0,
"react/react-in-jsx-scope": "warn", "react/react-in-jsx-scope": "warn",
"react/no-unescaped-entities": "off",
"react/jsx-filename-extension": [1, { extensions: [".tsx"] }], "react/jsx-filename-extension": [1, { extensions: [".tsx"] }],
}, },
}, },

View File

@ -1,6 +1,9 @@
import type { Theme } from "@/contexts/ThemeContext";
import { createHandler } from "@universal-middleware/fastify"; import { createHandler } from "@universal-middleware/fastify";
import { telefuncHandler } from "./server/telefunc-handler"; import { telefuncHandler } from "./server/telefunc-handler";
import { vikeHandler } from "./server/vike-handler"; import { vikeHandler } from "./server/vike-handler";
import fastifyCookie from "@fastify/cookie";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname } from "node:path"; import { dirname } from "node:path";
import Fastify from "fastify"; import Fastify from "fastify";
@ -12,9 +15,31 @@ const hmrPort = process.env.HMR_PORT ? parseInt(process.env.HMR_PORT, 10) : 2467
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
const root = __dirname; const root = __dirname;
declare global {
namespace Vike {
interface PageContext {
cookies: {
consent: {
analytics: boolean;
customization: boolean;
};
settings: {
theme: Theme;
};
};
}
}
}
async function startServer() { async function startServer() {
const app = Fastify(); const app = Fastify();
app.register(fastifyCookie, {
secret: "todo",
hook: "onRequest",
parseOptions: {},
});
// Avoid pre-parsing body, otherwise it will cause issue with universal handlers // Avoid pre-parsing body, otherwise it will cause issue with universal handlers
// This will probably change in the future though, you can follow https://github.com/magne4000/universal-middleware for updates // This will probably change in the future though, you can follow https://github.com/magne4000/universal-middleware for updates
app.removeAllContentTypeParsers(); app.removeAllContentTypeParsers();

View File

@ -1,3 +1,4 @@
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";
@ -56,7 +57,9 @@ function Header() {
<div className="relative flex grow basis-0 items-center"> <div className="relative flex grow basis-0 items-center">
<Link href="/" aria-label="Home page" className="flex items-center gap-2"> <Link href="/" aria-label="Home page" className="flex items-center gap-2">
<Logo className="h-9 w-auto" /> <Logo className="h-9 w-auto" />
<span className="hidden lg:inline text-2xl font-bold -tracking-tight">Memento Dev</span> <span className="hidden lg:inline text-2xl font-bold -tracking-tight text-slate-900 dark:text-slate-50">
Memento Dev
</span>
</Link> </Link>
</div> </div>
@ -74,30 +77,70 @@ function Header() {
); );
} }
function Footer() {
return (
<footer className="bg-slate-50 dark:bg-slate-950 text-slate-700 dark:text-slate-200">
<div className="mx-auto w-full flex flex-col max-w-8xl sm:px-2 lg:px-8 xl:px-12 py-8">
<section>
<header className="flex items-center gap-2 mb-2">
<Logo className="h-8 w-auto" />
<h2 className="font-display text-2xl">Memento Dev</h2>
</header>
<p>
Plateforme de ressources et documentations synthétiques et concises, conçue pour les développeurs ou
passionnés de l&lsquo;informatique en quête de savoir.
</p>
</section>
<hr className="my-6 border-slate-200 dark:border-slate-600" />
<section>
<header className="flex items-center gap-2">
<h2 className="font-display">&copy; 2022 - {new Date().getFullYear()} Memento Dev. Tous droits réservés</h2>
</header>
<p className="text-sm text-slate-500 dark:text-slate-400">
Memento Dev est une plateforme open-source, développée par{" "}
<Link href="https://gauthierdaniels.fr" className="font-bold">
Gauthier Daniels
</Link>
, soutenue et maintenue par une communauté de contributeurs passionnés.
</p>
</section>
</div>
</footer>
);
}
export default function DefaultLayout({ children }: { children: React.ReactNode }) { export default function DefaultLayout({ children }: { children: React.ReactNode }) {
const { urlPathname } = usePageContext(); const { urlPathname, cookies } = usePageContext();
const isHomePage = urlPathname === "/"; const isHomePage = urlPathname === "/";
return ( return (
<ThemeProvider> <CookiesContainer>
<div className="flex w-full flex-col font-sans"> <ThemeProvider defaultTheme={cookies.settings.theme}>
<Header /> <div className="flex w-full flex-col font-sans">
<Header />
{isHomePage && <Hero />} {isHomePage && <Hero />}
<div className="relative mx-auto w-full flex max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12"> <div className="relative mx-auto w-full flex max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
<div className="hidden lg:relative lg:block lg:flex-none"> <div className="hidden lg:relative lg:block lg:flex-none">
<div className="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 dark:hidden" /> <div className="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 dark:hidden" />
<div className="absolute top-16 right-0 bottom-0 hidden h-12 w-px bg-linear-to-t from-slate-800 dark:block" /> <div className="absolute top-16 right-0 bottom-0 hidden h-12 w-px bg-linear-to-t from-slate-800 dark:block" />
<div className="absolute top-28 right-0 bottom-0 hidden w-px bg-slate-800 dark:block" /> <div className="absolute top-28 right-0 bottom-0 hidden w-px bg-slate-800 dark:block" />
<div className="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-x-hidden overflow-y-auto py-16 pr-8 pl-0.5 xl:w-72 xl:pr-16"> <div className="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-x-hidden overflow-y-auto py-16 pr-8 pl-0.5 xl:w-72 xl:pr-16">
<Navigation /> <Navigation />
</div>
</div> </div>
<div className="grow">{children}</div>
</div> </div>
{children}
<Footer />
</div> </div>
</div> <ToastContainer />
<ToastContainer /> </ThemeProvider>
</ThemeProvider> </CookiesContainer>
); );
} }

View File

@ -0,0 +1,18 @@
import type { FastifyRequest, FastifyReply } from "fastify";
import { getContext } from "telefunc";
export function getTelefuncContext() {
const context = getContext<{
fastify: {
request: FastifyRequest;
reply: FastifyReply;
};
}>();
return {
...context,
reply: context.fastify.reply,
request: context.fastify.request,
};
}

View File

@ -2,12 +2,13 @@ const navigationsTypes = {
GLOBAL: "👋 Général", GLOBAL: "👋 Général",
CERTIFICATIONS: "🎓 Certifications", CERTIFICATIONS: "🎓 Certifications",
DOCUMENTATIONS: "📚 Documentations", DOCUMENTATIONS: "📚 Documentations",
OTHER: "🔗 Autres",
}; };
export type NavigationSection = { export type NavigationSection = {
title: string; title: string;
type: (typeof navigationsTypes)[keyof typeof navigationsTypes]; type: (typeof navigationsTypes)[keyof typeof navigationsTypes];
position: "start" | "auto"; position: "start" | "end" | "auto";
links: NavigationLink[]; links: NavigationLink[];
}; };
@ -47,6 +48,15 @@ export const navigation: NavigationSection[] = [
{ title: "Partages et réutilisations", href: "/docs/communaute/partages", subitems: [] }, { title: "Partages et réutilisations", href: "/docs/communaute/partages", subitems: [] },
], ],
}, },
{
title: "Légal",
type: navigationsTypes.OTHER,
position: "end",
links: [
{ title: "Mentions légales", href: "/mentions-legales", subitems: [] },
{ title: "Politique de confidentialité", href: "/politique-de-confidentialite", subitems: [] },
],
},
{ {
title: "Développeur Web et Web Mobile", title: "Développeur Web et Web Mobile",
type: navigationsTypes.CERTIFICATIONS, type: navigationsTypes.CERTIFICATIONS,

View File

@ -7,6 +7,7 @@
"prod": "npm-run-all build preview" "prod": "npm-run-all build preview"
}, },
"dependencies": { "dependencies": {
"@fastify/cookie": "^11.0.2",
"@fastify/middie": "^9.0.3", "@fastify/middie": "^9.0.3",
"@fastify/static": "^8.1.1", "@fastify/static": "^8.1.1",
"@fontsource-variable/inter": "^5.2.5", "@fontsource-variable/inter": "^5.2.5",

View File

@ -1,10 +1,28 @@
import { usePageContext } from "vike-react/usePageContext";
import logoUrl from "@/assets/logo.svg"; import logoUrl from "@/assets/logo.svg";
import React from "react"; import React from "react";
export default function HeadDefault() { export default function HeadDefault() {
const { cookies } = usePageContext();
return ( return (
<> <>
<link rel="icon" href={logoUrl} /> <link rel="icon" href={logoUrl} />
{cookies.consent.analytics && (
<>
<script defer src="https://cloud.umami.is/script.js" data-website-id="ba70261e-d145-4dd1-b0e8-27cbf4927b74" />
<script async src={`https://www.googletagmanager.com/gtag/js?id=GTM-NRMKQ68K`} />
<script
dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GTM-NRMKQ68K');`,
}}
/>
</>
)}
</> </>
); );
} }

View File

@ -25,7 +25,7 @@ export default {
image: "/og.webp", image: "/og.webp",
// prerender: true, passToClient: ["cookies"],
prefetchStaticAssets: "hover", prefetchStaticAssets: "hover",
extends: vikeReact, extends: vikeReact,

View File

@ -0,0 +1,52 @@
import React from "react";
export function Page() {
return (
<div className="px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16 flex flex-col gap-4 text-slate-700 dark:text-slate-300">
<h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">Mentions légales</h1>
<section>
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">Éditeur du site</h2>
<p>
<span className="font-bold">Nom :</span> <strong className="font-normal">Gauthier Daniels</strong>
</p>
<p>
<span className="font-bold">Adresse physique :</span>{" "}
<strong className="font-normal">689 Chemin Latéral, 45240 La Ferté Saint-Aubin</strong>
</p>
<p>
<span className="font-bold">Adresse email :</span>{" "}
<strong className="font-normal">gauthier@gauthierdaniels.fr</strong>
</p>
<p>
<span className="font-bold">Téléphone :</span> <strong className="font-normal">+33 6 52 84 92 41</strong>
</p>
</section>
<section>
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">Directeur de la publication</h2>
<p>
<span className="font-bold">Nom :</span> <strong className="font-normal">Gauthier Daniels</strong>
</p>
<p>
<span className="font-bold">Adresse email :</span>{" "}
<strong className="font-normal">gauthier@gauthierdaniels.fr</strong>
</p>
</section>
<section>
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">Hébergement du site</h2>
<p>
<span className="font-bold">Nom :</span> <strong className="font-normal">Infomaniak Network SA</strong>
</p>
<p>
<span className="font-bold">Site internet :</span> <strong className="font-normal">www.infomaniak.com</strong>
</p>
<p>
<span className="font-bold">Adresse physique :</span>{" "}
<strong className="font-normal">Rue Eugène Marziano 25, 1227 Les Acacias (GE), Suisse</strong>
</p>
</section>
</div>
);
}

View File

@ -0,0 +1,189 @@
import { CookiesContext } from "@/components/common/Cookies";
import { Button } from "@/components/syntax/Button";
import { Link } from "@/components/common/Link";
import React, { useContext } from "react";
export function Page() {
const { setIsOpen } = useContext(CookiesContext);
return (
<div className="px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16 flex flex-col gap-4 text-slate-700 dark:text-slate-300">
<h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">
Politique de confidentialité
</h1>
<section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">1. Introduction</h2>
<p>
Sur <strong>Memento Dev</strong>, qui est un site à but de documentation, je prends très au sérieux la
protection de votre vie privée et de vos données personnelles.
</p>
<p>
Cette politique de confidentialité explique comment je collecte, utilise et protège les informations des
utilisateurs de ce site.
</p>
</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>
<p>
Mon site ne collecte aucune donnée personnelle de manière directe. Cependant, j'utilise des outils externes
pour améliorer votre expérience utilisateur et analyser l'utilisation du site.
</p>
<section className="ml-4">
<h3 className="font-display text-lg text-slate-900 dark:text-slate-100">a. Google Analytics</h3>
<p>
J'utilise <strong>Google Analytics</strong> pour analyser le trafic et l'utilisation de mon site. Les
données collectées par Google Analytics sont anonymisées et ne sont pas partagées avec des tiers.
</p>
</section>
<section className="ml-4">
<h3 className="font-display text-lg text-slate-900 dark:text-slate-100">b. Umami</h3>
<p>
<strong>Umami</strong> est un autre outil d'analyse que j'utilise pour comprendre comment les visiteurs
interagissent avec mon site. Comme <strong>Google Analytics</strong>, les données collectées sont
anonymisées et ne sont pas tranmises à des tiers.
</p>
</section>
<section className="ml-4">
<h3 className="font-display text-lg text-slate-900 dark:text-slate-100">c. Cookie "theme"</h3>
<p>
J'utilise et dépose un cookie nommé "theme" pour mémoriser votre préférence de thème (clair ou sombre). Ce
cookie est utilisé uniquement pour personnaliser votre expérience utilisateur et n'est pas utilisé à des
fins de suivi ou de marketing.
</p>
</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>
<p>
Ce site utilise des cookies pour améliorer votre expérience utilisateur et analyser l'utilisation du site. Les
cookies sont de petits fichiers texte stockés sur votre appareil lorsque vous visitez un site web. Ils
permettent de mémoriser vos préférences et d'analyser le trafic du site.
</p>
<p>
Vous pouvez gérer vos préférences de cookies directement via les paramètres de votre navigateur. La plupart
des navigateurs vous permettent de refuser ou de supprimer les cookies. Cependant, cela peut affecter votre
expérience sur le site et certaines fonctionnalités peuvent ne pas fonctionner correctement (comme la
personnalisation du thème par exemple).
</p>
<p>
Pour reconfigurer les cookies, vous pouvez appuyer sur le bouton "Paramétrer les cookies" à la suite de ce
paragraphe.
</p>
<Button variant="secondary" className="w-max max-w-full" onClick={() => setIsOpen(true)}>
Paramétrer les cookies
</Button>
<p>
Pour plus d'informations sur la gestion des cookies, vous pouvez consulter la documentation de votre
navigateur. Voici quelques liens utiles :
</p>
<ul className="list-disc pl-4">
<li>
<Link
href="https://support.google.com/chrome/answer/95647?hl=fr"
className="text-violet-500 dark:text-violet-400"
>
Google Chrome
</Link>
</li>
<li>
<Link
href="https://support.mozilla.org/fr/kb/activer-desactiver-cookies"
className="text-violet-500 dark:text-violet-400"
>
Mozilla Firefox
</Link>
</li>
<li>
<Link
href="https://support.microsoft.com/fr-fr/help/278835/how-to-delete-cookie-files-in-internet-explorer"
className="text-violet-500 dark:text-violet-400"
>
Internet Explorer
</Link>
</li>
<li>
<Link href="https://support.apple.com/fr-fr/HT201265" className="text-violet-500 dark:text-violet-400">
Safari
</Link>
</li>
<li>
<Link
href="https://support.microsoft.com/fr-fr/help/278835/how-to-delete-cookie-files-in-internet-explorer"
className="text-violet-500 dark:text-violet-400"
>
Microsoft Edge
</Link>
</li>
</ul>
</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>
<p>
Les données collectées par les outils d'analyse sont utilisées uniquement pour améliorer le site Memento Dev
et comprendre comment les visiteurs l'utilisent. Je n'utilise pas ces données à des fins commerciales ou pour
cibler des publicités.
</p>
</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>
<p>
Les données collectées par <strong>Google Analytics</strong> et <strong>Umami</strong> sont anonymisées et
stockées de manière sécurisée par ces services. Je ne stocke aucune donnée personnelle sur mes propres
serveurs. Pour plus d'informations sur la manière dont ces services protègent vos données, veuillez consulter
leurs politiques de confidentialité respectives.
</p>
</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>
<p>
Étant donné que je ne stocke aucune donnée personnelle, je ne suis pas en mesure de répondre aux demandes
d'accès, de rectification ou de suppression de données personnelles. Cependant, vous pouvez gérer vos
préférences de cookies directement via les paramètres de votre navigateur. Pour toute question concernant vos
droits, veuillez me contacter à <strong>gauthier@gauthierdaniels.fr</strong>.
</p>
</section>
<section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">
7. Modifications de la Politique de Confidentialité
</h2>
<p>
Je me réserve le droit de modifier cette politique de confidentialité à tout moment. Les modifications seront
publiées sur cette page et entreront en vigueur immédiatement. Je vous encourage à consulter régulièrement
cette page pour rester informé de mes pratiques en matière de confidentialité.
</p>
</section>
<section className="flex flex-col gap-2">
<h2 className="font-display text-xl text-slate-900 dark:text-slate-100">8. Contact</h2>
<p>
Si vous avez des questions ou des préoccupations concernant ma politique de confidentialité, veuillez me
contacter à l'adresse suivante : <strong>gauthier@gauthierdaniels.fr</strong>.
</p>
</section>
</div>
);
}

39
app/pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@fastify/cookie':
specifier: ^11.0.2
version: 11.0.2
'@fastify/middie': '@fastify/middie':
specifier: ^9.0.3 specifier: ^9.0.3
version: 9.0.3 version: 9.0.3
@ -94,7 +97,7 @@ importers:
version: 1.2.1 version: 1.2.1
telefunc: telefunc:
specifier: ^0.1.87 specifier: ^0.1.87
version: 0.1.87(@babel/core@7.26.10)(@babel/parser@7.27.0)(@babel/types@7.27.0)(react-streaming@0.3.50(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) version: 0.1.87(@babel/core@7.7.4)(@babel/parser@7.27.0)(@babel/types@7.27.0)(react-streaming@0.3.50(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
unplugin-fonts: unplugin-fonts:
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1(vite@6.2.6(@types/node@18.19.86)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.3)) version: 1.3.1(vite@6.2.6(@types/node@18.19.86)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.3))
@ -1301,6 +1304,9 @@ packages:
'@fastify/ajv-compiler@4.0.2': '@fastify/ajv-compiler@4.0.2':
resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==}
'@fastify/cookie@11.0.2':
resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
'@fastify/error@4.1.0': '@fastify/error@4.1.0':
resolution: {integrity: sha512-KeFcciOr1eo/YvIXHP65S94jfEEqn1RxTRBT1aJaHxY5FK0/GDXYozsQMMWlZoHgi8i0s+YtrLsgj/JkUUjSkQ==} resolution: {integrity: sha512-KeFcciOr1eo/YvIXHP65S94jfEEqn1RxTRBT1aJaHxY5FK0/GDXYozsQMMWlZoHgi8i0s+YtrLsgj/JkUUjSkQ==}
@ -9754,6 +9760,11 @@ snapshots:
ajv-formats: 3.0.1(ajv@8.17.1) ajv-formats: 3.0.1(ajv@8.17.1)
fast-uri: 3.0.6 fast-uri: 3.0.6
'@fastify/cookie@11.0.2':
dependencies:
cookie: 1.0.2
fastify-plugin: 5.0.1
'@fastify/error@4.1.0': {} '@fastify/error@4.1.0': {}
'@fastify/fast-json-stringify-compiler@5.0.3': '@fastify/fast-json-stringify-compiler@5.0.3':
@ -10506,10 +10517,10 @@ snapshots:
dependencies: dependencies:
'@types/yargs-parser': 21.0.3 '@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)': '@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies: dependencies:
'@typescript-eslint/experimental-utils': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/experimental-utils': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/parser': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.24.0(jiti@2.4.2) eslint: 9.24.0(jiti@2.4.2)
functional-red-black-tree: 1.0.1 functional-red-black-tree: 1.0.1
regexpp: 3.2.0 regexpp: 3.2.0
@ -12828,13 +12839,13 @@ snapshots:
eslint-config-react-app@5.2.1(@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(babel-eslint@10.0.3(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-flowtype@3.13.0(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-jsx-a11y@6.2.3(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-react-hooks@1.7.0(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-react@7.16.0(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3): eslint-config-react-app@5.2.1(@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(babel-eslint@10.0.3(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-flowtype@3.13.0(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-jsx-a11y@6.2.3(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-react-hooks@1.7.0(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-react@7.16.0(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3):
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/parser': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
babel-eslint: 10.0.3(eslint@9.24.0(jiti@2.4.2)) babel-eslint: 10.0.3(eslint@9.24.0(jiti@2.4.2))
confusing-browser-globals: 1.0.11 confusing-browser-globals: 1.0.11
eslint: 9.24.0(jiti@2.4.2) eslint: 9.24.0(jiti@2.4.2)
eslint-plugin-flowtype: 3.13.0(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-flowtype: 3.13.0(eslint@9.24.0(jiti@2.4.2))
eslint-plugin-import: 2.18.2(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-import: 2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.2.3(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.2.3(eslint@9.24.0(jiti@2.4.2))
eslint-plugin-react: 7.16.0(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react: 7.16.0(eslint@9.24.0(jiti@2.4.2))
eslint-plugin-react-hooks: 1.7.0(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react-hooks: 1.7.0(eslint@9.24.0(jiti@2.4.2))
@ -12859,11 +12870,11 @@ snapshots:
schema-utils: 2.7.1 schema-utils: 2.7.1
webpack: 4.41.2 webpack: 4.41.2
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.24.0(jiti@2.4.2)): eslint-module-utils@2.12.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.24.0(jiti@2.4.2)):
dependencies: dependencies:
debug: 3.2.7(supports-color@6.1.0) debug: 3.2.7(supports-color@6.1.0)
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/parser': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.24.0(jiti@2.4.2) eslint: 9.24.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
transitivePeerDependencies: transitivePeerDependencies:
@ -12874,7 +12885,7 @@ snapshots:
eslint: 9.24.0(jiti@2.4.2) eslint: 9.24.0(jiti@2.4.2)
lodash: 4.17.21 lodash: 4.17.21
eslint-plugin-import@2.18.2(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)): eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)):
dependencies: dependencies:
array-includes: 3.1.8 array-includes: 3.1.8
contains-path: 0.1.0 contains-path: 0.1.0
@ -12882,14 +12893,14 @@ snapshots:
doctrine: 1.5.0 doctrine: 1.5.0
eslint: 9.24.0(jiti@2.4.2) eslint: 9.24.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.24.0(jiti@2.4.2)) eslint-module-utils: 2.12.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.24.0(jiti@2.4.2))
has: 1.0.4 has: 1.0.4
minimatch: 3.1.2 minimatch: 3.1.2
object.values: 1.2.1 object.values: 1.2.1
read-pkg-up: 2.0.0 read-pkg-up: 2.0.0
resolve: 1.22.10 resolve: 1.22.10
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/parser': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- eslint-import-resolver-typescript - eslint-import-resolver-typescript
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
@ -16363,7 +16374,7 @@ snapshots:
dependencies: dependencies:
'@babel/core': 7.7.4 '@babel/core': 7.7.4
'@svgr/webpack': 4.3.3 '@svgr/webpack': 4.3.3
'@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/parser': 2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
babel-eslint: 10.0.3(eslint@9.24.0(jiti@2.4.2)) babel-eslint: 10.0.3(eslint@9.24.0(jiti@2.4.2))
babel-jest: 24.9.0(@babel/core@7.7.4) babel-jest: 24.9.0(@babel/core@7.7.4)
@ -16379,7 +16390,7 @@ snapshots:
eslint-config-react-app: 5.2.1(@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(babel-eslint@10.0.3(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-flowtype@3.13.0(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-jsx-a11y@6.2.3(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-react-hooks@1.7.0(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-react@7.16.0(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) eslint-config-react-app: 5.2.1(@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(babel-eslint@10.0.3(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-flowtype@3.13.0(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-import@2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-jsx-a11y@6.2.3(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-react-hooks@1.7.0(eslint@9.24.0(jiti@2.4.2)))(eslint-plugin-react@7.16.0(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
eslint-loader: 3.0.2(eslint@9.24.0(jiti@2.4.2))(webpack@4.41.2) eslint-loader: 3.0.2(eslint@9.24.0(jiti@2.4.2))(webpack@4.41.2)
eslint-plugin-flowtype: 3.13.0(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-flowtype: 3.13.0(eslint@9.24.0(jiti@2.4.2))
eslint-plugin-import: 2.18.2(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-import: 2.18.2(@typescript-eslint/parser@2.34.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.2.3(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.2.3(eslint@9.24.0(jiti@2.4.2))
eslint-plugin-react: 7.16.0(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react: 7.16.0(eslint@9.24.0(jiti@2.4.2))
eslint-plugin-react-hooks: 1.7.0(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react-hooks: 1.7.0(eslint@9.24.0(jiti@2.4.2))
@ -17435,7 +17446,7 @@ snapshots:
tapable@2.2.1: {} tapable@2.2.1: {}
telefunc@0.1.87(@babel/core@7.26.10)(@babel/parser@7.27.0)(@babel/types@7.27.0)(react-streaming@0.3.50(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0): telefunc@0.1.87(@babel/core@7.7.4)(@babel/parser@7.27.0)(@babel/types@7.27.0)(react-streaming@0.3.50(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
'@brillout/import': 0.2.6 '@brillout/import': 0.2.6
'@brillout/json-serializer': 0.5.15 '@brillout/json-serializer': 0.5.15
@ -17444,7 +17455,7 @@ snapshots:
es-module-lexer: 1.6.0 es-module-lexer: 1.6.0
ts-morph: 19.0.0 ts-morph: 19.0.0
optionalDependencies: optionalDependencies:
'@babel/core': 7.26.10 '@babel/core': 7.7.4
'@babel/parser': 7.27.0 '@babel/parser': 7.27.0
'@babel/types': 7.27.0 '@babel/types': 7.27.0
react: 19.1.0 react: 19.1.0

View File

@ -0,0 +1,22 @@
import type { Theme } from "@/contexts/ThemeContext";
import { getTelefuncContext } from "@/lib/getTelefuncContext";
import { CookieParser } from "@/services/CookieParser";
export async function onUpdateThemeCookie(value: Theme) {
const context = getTelefuncContext();
const { reply } = context;
CookieParser.set(reply, "theme", value, 365);
return { ok: true, message: "Updated theme cookie", value };
}
export async function onDeleteThemeCookie() {
const context = getTelefuncContext();
const { reply } = context;
CookieParser.delete(reply, "theme");
return { ok: true, message: "Deleted theme cookie" };
}

View File

@ -1,5 +1,8 @@
import { onUpdateThemeCookie, onDeleteThemeCookie } from "@/providers/ThemeProvider.telefunc";
import { ThemeContext, type Theme } from "@/contexts/ThemeContext"; import { ThemeContext, type Theme } from "@/contexts/ThemeContext";
import { usePageContext } from "vike-react/usePageContext";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { toast } from "react-toastify";
type ThemeProviderProps = { type ThemeProviderProps = {
children: React.ReactNode; children: React.ReactNode;
@ -7,6 +10,7 @@ type ThemeProviderProps = {
}; };
export function ThemeProvider(props: ThemeProviderProps) { export function ThemeProvider(props: ThemeProviderProps) {
const { cookies } = usePageContext();
const [theme, setTheme] = useState<Theme>(props.defaultTheme || "light"); const [theme, setTheme] = useState<Theme>(props.defaultTheme || "light");
useEffect(() => { useEffect(() => {
@ -14,7 +18,17 @@ export function ThemeProvider(props: ThemeProviderProps) {
rootElement.classList.toggle("dark", theme === "dark"); rootElement.classList.toggle("dark", theme === "dark");
rootElement.classList.toggle("light", theme === "light"); rootElement.classList.toggle("light", theme === "light");
}, [theme]);
if (cookies.consent.customization) {
onUpdateThemeCookie(theme).catch(() => {
toast.error("Erreur lors de la mise à jour du cookie de thème");
});
} else {
onDeleteThemeCookie().catch(() => {
toast.error("Erreur lors de la suppression du cookie de thème");
});
}
}, [theme, cookies.consent.customization]);
return <ThemeContext.Provider value={{ theme, setTheme }}>{props.children}</ThemeContext.Provider>; return <ThemeContext.Provider value={{ theme, setTheme }}>{props.children}</ThemeContext.Provider>;
} }

View File

@ -1,10 +1,29 @@
/// <reference lib="webworker" /> /// <reference lib="webworker" />
import { renderPage } from "vike/server";
// TODO: stop using universal-middleware and directly integrate server middlewares instead. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.) // TODO: stop using universal-middleware and directly integrate server middlewares instead. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.)
import type { Get, UniversalHandler } from "@universal-middleware/core"; import type { Get, UniversalHandler } from "@universal-middleware/core";
import { CookieParser } from "@/services/CookieParser";
import { renderPage } from "vike/server";
export const vikeHandler: Get<[], UniversalHandler> = () => async (request, context, runtime) => { export const vikeHandler: Get<[], UniversalHandler> = () => async (request, context, runtime) => {
const pageContextInit = { ...context, ...runtime, urlOriginal: request.url, headersOriginal: request.headers }; const cookies = new CookieParser(request.headers.get("cookie") || "");
const pageContextInit = {
...context,
...runtime,
urlOriginal: request.url,
headersOriginal: request.headers,
cookies: {
consent: {
analytics: cookies.get<boolean>("analytics", (value) => value === "true") || false,
customization: cookies.get<boolean>("customization", (value) => value === "true") || false,
},
settings: {
theme: cookies.get("theme") || "light",
},
},
};
const pageContext = await renderPage(pageContextInit); const pageContext = await renderPage(pageContextInit);
const response = pageContext.httpResponse; const response = pageContext.httpResponse;

View File

@ -0,0 +1,70 @@
import type { PageContext } from "vike/types";
import { FastifyReply } from "fastify";
type ConsentCookies = keyof PageContext["cookies"]["consent"];
type SettingsCookie = keyof PageContext["cookies"]["settings"];
type CookieKeys = ConsentCookies | SettingsCookie;
export class CookieParser {
private rawCookies: string;
private cookies: Record<string, string>;
constructor(rawCookies: string) {
this.rawCookies = rawCookies;
this.cookies = {};
this.parse();
}
public static buildOptions(daysDuration: number | Date) {
let expires: Date;
if (daysDuration instanceof Date) {
expires = daysDuration;
} else {
expires = new Date(Date.now() + 60 * 60 * 24 * daysDuration * 1000);
}
return {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
expires,
};
}
parse() {
this.cookies = this.rawCookies.split("; ").reduce(
(acc, cookie) => {
const [key, value] = cookie.split("=");
acc[key] = decodeURIComponent(value);
return acc;
},
{} as Record<string, string>,
);
}
get(key: CookieKeys): string | undefined;
get<T = unknown>(key: CookieKeys, formatter: (value: string) => T): T | undefined;
get<T = unknown>(key: CookieKeys, formatter?: (value: string) => T): T | string | undefined {
const value = this.cookies[key];
if (formatter) return formatter(value);
return value;
}
static set(reply: FastifyReply, key: CookieKeys, value: string, daysDuration = 30): FastifyReply {
const options = CookieParser.buildOptions(daysDuration);
reply.setCookie(key, value, options);
return reply;
}
static delete(reply: FastifyReply, key: CookieKeys): FastifyReply {
const options = CookieParser.buildOptions(new Date("1900-01-01"));
reply.setCookie(key, "", options);
return reply;
}
}