Merge pull request 'feat/analytics' (#9) from feat/analytics into main
Some checks failed
Update Memento Dev on VPS / deploy (push) Has been cancelled
Some checks failed
Update Memento Dev on VPS / deploy (push) Has been cancelled
Reviewed-on: #9
This commit is contained in:
commit
c0b4c771d4
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 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" };
|
||||||
|
}
|
||||||
192
app/components/common/Cookies.tsx
Normal file
192
app/components/common/Cookies.tsx
Normal 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‘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.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‘ai faim !
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
app/components/common/Toggle.tsx
Normal file
45
app/components/common/Toggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 = {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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")} />
|
||||||
|
|||||||
@ -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"] }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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‘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">© 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
18
app/lib/getTelefuncContext.ts
Normal file
18
app/lib/getTelefuncContext.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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');`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export default {
|
|||||||
|
|
||||||
image: "/og.webp",
|
image: "/og.webp",
|
||||||
|
|
||||||
// prerender: true,
|
passToClient: ["cookies"],
|
||||||
prefetchStaticAssets: "hover",
|
prefetchStaticAssets: "hover",
|
||||||
|
|
||||||
extends: vikeReact,
|
extends: vikeReact,
|
||||||
|
|||||||
52
app/pages/mentions-legales/+Page.tsx
Normal file
52
app/pages/mentions-legales/+Page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
189
app/pages/politique-de-confidentialite/+Page.tsx
Normal file
189
app/pages/politique-de-confidentialite/+Page.tsx
Normal 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
39
app/pnpm-lock.yaml
generated
@ -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
|
||||||
|
|||||||
22
app/providers/ThemeProvider.telefunc.ts
Normal file
22
app/providers/ThemeProvider.telefunc.ts
Normal 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" };
|
||||||
|
}
|
||||||
@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
70
app/services/CookieParser.ts
Normal file
70
app/services/CookieParser.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user