freezmo/components/CookieConsent.tsx
Martin Dvorak 464781a7fd Privacy policy, terms of service, cookie policy + a cookie notice
Three plain-language documents in both app languages (lib/legal.ts, rendered
by components/LegalPage.tsx): /privacy describes the local-first storage, the
key never leaving the browser and the three opt-in server copies (sync,
reminders, reports); /terms covers the GPL-3.0-only software, your own Ollama
key, fair use and liability; /cookies states what is true — no cookies at all
(no Set-Cookie, no document.cookie), strictly-necessary local storage instead,
no tracking.

A cookie notice tells every new browser the same thing once and remembers the
dismissal (freezmo.consent.v1). It sits at z-44: below the immersive study
feeds so it can never cover a session, above the phone tab bar. Settings links
all three documents. The regression suite checks the pages render with real
sections and that dismissing the notice sticks.

Version 0.1.7, SW -> freezmo-v53.
2026-10-12 21:26:55 +00:00

62 lines
1.8 KiB
TypeScript

"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useI18n } from "@/lib/i18n";
const STORAGE_KEY = "freezmo.consent.v1";
/**
* The cookie notice. There are no cookies (see /cookies), so this is an
* honest "here is what we store in your browser" line, dismissed once and
* remembered. It sits below the immersive feeds in the stack (z-44) so it can
* never cover a study session, and above the phone tab bar.
*/
export function CookieConsent() {
const { t } = useI18n();
const [visible, setVisible] = useState(false);
useEffect(() => {
try {
setVisible(window.localStorage.getItem(STORAGE_KEY) === null);
} catch {
/* private mode: no storage, no banner */
}
}, []);
if (!visible) return null;
const dismiss = () => {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ at: Date.now() }));
} catch {
/* private mode */
}
setVisible(false);
};
return (
<div className="pointer-events-none fixed inset-x-0 bottom-[calc(env(safe-area-inset-bottom,0px)+5.25rem)] z-[44] flex justify-center px-4 sm:bottom-6">
<div
role="region"
aria-label={t("consent.title")}
className="card pointer-events-auto flex max-w-xl flex-wrap items-center gap-3 p-3"
>
<p className="min-w-0 flex-1 text-[12.5px] leading-snug text-[var(--text-muted)]">
{t("consent.body")}{" "}
<Link href="/cookies" className="font-semibold text-[var(--text)] underline decoration-dotted">
{t("consent.link")}
</Link>
</p>
<button
type="button"
onClick={dismiss}
className="btn-primary touch-target h-10 shrink-0 rounded-xl px-4 text-[13px] font-semibold"
>
{t("consent.ok")}
</button>
</div>
</div>
);
}