feat(web): privacy-first cookie banner
- New CookieBanner client component anchored at the bottom of the locale layout, dismissed via Button click and persisted via localStorage key ketopath:cookie-notice-ack (no cookie set for the dismissal itself) - Italian copy: only essential cookies for authentication/session, no analytics or third-party trackers (consistent with the ADR 0001 stance and CLAUDE.md "Privacy first" non-negotiable) - Layout body now uses bg-background / text-foreground tokens so dark mode toggling will work the day we add it Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c85179659a
commit
50d68fc522
3 files changed
+50
-1
No files matched your search
@@ -9,6 +9,11 @@
|
|||||||
"signOut": "Esci",
|
"signOut": "Esci",
|
||||||
"completeProfile": "Completa il tuo profilo"
|
"completeProfile": "Completa il tuo profilo"
|
||||||
},
|
},
|
||||||
|
"CookieBanner": {
|
||||||
|
"aria": "Informativa cookie",
|
||||||
|
"message": "Usiamo solo cookie essenziali per l'autenticazione e la sessione. Niente analytics o tracker di terze parti.",
|
||||||
|
"dismiss": "Ho capito"
|
||||||
|
},
|
||||||
"Metadata": {
|
"Metadata": {
|
||||||
"title": "KetoPath — Chetogenica e digiuno intermittente personalizzati",
|
"title": "KetoPath — Chetogenica e digiuno intermittente personalizzati",
|
||||||
"description": "Web app italiana per la gestione strutturata di chetogenica e digiuno intermittente, con piani adattivi, tracking e lista della spesa automatica."
|
"description": "Web app italiana per la gestione strutturata di chetogenica e digiuno intermittente, con piani adattivi, tracking e lista della spesa automatica."
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextIntlClientProvider } from 'next-intl';
|
|||||||
import { getMessages, getTranslations, setRequestLocale } from 'next-intl/server';
|
import { getMessages, getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { CookieBanner } from '@/components/cookie-banner';
|
||||||
import { locales, type Locale } from '@/i18n';
|
import { locales, type Locale } from '@/i18n';
|
||||||
|
|
||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
@@ -48,9 +49,10 @@ export default async function LocaleLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} className={inter.variable}>
|
<html lang={locale} className={inter.variable}>
|
||||||
<body className="min-h-screen bg-white font-sans text-slate-900 antialiased">
|
<body className="bg-background text-foreground min-h-screen font-sans antialiased">
|
||||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||||
{children}
|
{children}
|
||||||
|
<CookieBanner />
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ketopath:cookie-notice-ack';
|
||||||
|
|
||||||
|
export function CookieBanner() {
|
||||||
|
const t = useTranslations('CookieBanner');
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if (window.localStorage.getItem(STORAGE_KEY) !== 'true') {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, 'true');
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="region"
|
||||||
|
aria-label={t('aria')}
|
||||||
|
className="bg-background fixed inset-x-0 bottom-0 z-50 border-t p-4 shadow-lg"
|
||||||
|
>
|
||||||
|
<div className="mx-auto flex max-w-3xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p className="text-muted-foreground text-sm">{t('message')}</p>
|
||||||
|
<Button type="button" size="sm" onClick={dismiss}>
|
||||||
|
{t('dismiss')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user