Files
portfolio/app/components/ConsentBanner.tsx
Cursor Agent a617f6eb92 feat(i18n): centralize more UI texts in messages
Move hardcoded labels/strings in About, Projects, Contact form, Footer and Consent banner into next-intl message files (en/de) so content is maintained in one place.

Co-authored-by: dennis <dennis@konkol.net>
2026-01-15 10:03:32 +00:00

117 lines
4.3 KiB
TypeScript

"use client";
import React, { useState } from "react";
import { useConsent, type ConsentState } from "./ConsentProvider";
import { useTranslations } from "next-intl";
export default function ConsentBanner() {
const { consent, ready, setConsent } = useConsent();
const [draft, setDraft] = useState<ConsentState>({ analytics: false, chat: false });
const [minimized, setMinimized] = useState(false);
const t = useTranslations("consent");
// Avoid hydration mismatch + avoid "flash then disappear":
// Only decide whether to show the banner after consent has been read client-side.
const shouldShow = ready && consent === null;
if (!shouldShow) return null;
const s = {
title: t("title"),
description: t("description"),
essential: t("essential"),
analytics: t("analytics"),
chat: t("chat"),
alwaysOn: t("alwaysOn"),
acceptAll: t("acceptAll"),
acceptSelected: t("acceptSelected"),
rejectAll: t("rejectAll"),
hide: t("hide"),
};
if (minimized) {
return (
<div className="fixed bottom-4 right-4 z-[60]">
<button
type="button"
onClick={() => setMinimized(false)}
className="px-4 py-2 rounded-full bg-white/80 backdrop-blur-xl border border-white/60 shadow-lg text-stone-800 font-semibold hover:bg-white transition-colors"
aria-label="Open privacy settings"
>
{s.title}
</button>
</div>
);
}
return (
<div className="fixed bottom-4 right-4 z-[60] max-w-[calc(100vw-2rem)]">
<div className="w-[360px] max-w-full bg-white/85 backdrop-blur-xl border border-white/60 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.14)] p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-base font-bold text-stone-900">{s.title}</div>
<p className="text-xs text-stone-600 mt-1 leading-snug">{s.description}</p>
</div>
<button
type="button"
onClick={() => setMinimized(true)}
className="shrink-0 text-xs text-stone-500 hover:text-stone-900 transition-colors"
aria-label="Minimize privacy banner"
title="Minimize"
>
{s.hide}
</button>
</div>
<div className="mt-3 space-y-2">
<div className="flex items-center justify-between gap-3">
<div className="text-xs font-semibold text-stone-800">{s.essential}</div>
<div className="text-[11px] text-stone-500">{s.alwaysOn}</div>
</div>
<label className="flex items-center justify-between gap-3 py-1">
<span className="text-sm font-semibold text-stone-800">{s.analytics}</span>
<input
type="checkbox"
checked={draft.analytics}
onChange={(e) => setDraft((p) => ({ ...p, analytics: e.target.checked }))}
className="w-4 h-4 accent-stone-900"
/>
</label>
<label className="flex items-center justify-between gap-3 py-1">
<span className="text-sm font-semibold text-stone-800">{s.chat}</span>
<input
type="checkbox"
checked={draft.chat}
onChange={(e) => setDraft((p) => ({ ...p, chat: e.target.checked }))}
className="w-4 h-4 accent-stone-900"
/>
</label>
</div>
<div className="mt-3 flex flex-col gap-2">
<button
onClick={() => setConsent({ analytics: true, chat: true })}
className="px-4 py-2 rounded-xl bg-stone-900 text-stone-50 font-semibold hover:bg-stone-800 transition-colors"
>
{s.acceptAll}
</button>
<button
onClick={() => setConsent(draft)}
className="px-4 py-2 rounded-xl bg-white border border-stone-200 text-stone-800 font-semibold hover:bg-stone-50 transition-colors"
>
{s.acceptSelected}
</button>
<button
onClick={() => setConsent({ analytics: false, chat: false })}
className="px-4 py-2 rounded-xl bg-transparent text-stone-600 font-semibold hover:text-stone-900 transition-colors"
>
{s.rejectAll}
</button>
</div>
</div>
</div>
);
}