Use next-intl translations instead of reading NEXT_LOCALE cookie once, so banner text updates immediately when switching languages. fix(activity-feed): make loading UI match dark theme Avoid the white loading card on hard reload by using the same dark styling as the normal feed. Co-authored-by: dennis <dennis@konkol.net>
113 lines
4.1 KiB
TypeScript
113 lines
4.1 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, setConsent } = useConsent();
|
|
const [draft, setDraft] = useState<ConsentState>({ analytics: false, chat: false });
|
|
const [minimized, setMinimized] = useState(false);
|
|
const t = useTranslations("consent");
|
|
|
|
const shouldShow = consent === null;
|
|
if (!shouldShow) return null;
|
|
|
|
const s = {
|
|
title: t("title"),
|
|
description: t("description"),
|
|
essential: t("essential"),
|
|
analytics: t("analytics"),
|
|
chat: t("chat"),
|
|
acceptAll: t("acceptAll"),
|
|
acceptSelected: t("acceptSelected"),
|
|
rejectAll: t("rejectAll"),
|
|
};
|
|
|
|
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"
|
|
>
|
|
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">Always on</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>
|
|
);
|
|
}
|
|
|