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",
|
||||
"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": {
|
||||
"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."
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { CookieBanner } from '@/components/cookie-banner';
|
||||
import { locales, type Locale } from '@/i18n';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
@@ -48,9 +49,10 @@ export default async function LocaleLayout({
|
||||
|
||||
return (
|
||||
<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}>
|
||||
{children}
|
||||
<CookieBanner />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</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