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:
lucianoandClaude Opus 4.7 committed 2026-04-29 15:48:32 +02:00
1 parent c85179659a
commit 50d68fc522
3 files changed
+50 -1

No files matched your search

+3 -1
View File
@@ -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>
+42
View File
@@ -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>
);
}