All checks were successful
Gitea CI / test-build (push) Successful in 11m38s
- Add WebGL support detection in ShaderGradientBackground to prevent console errors - Add .catch() fallback to ShaderGradientBackground dynamic import - Remove hardcoded aria-label from consent banner minimize button (fixes label-content-name-mismatch) - Use rewrite instead of redirect for root locale routing (eliminates one redirect hop) - Change n8n API cache headers from no-store to no-cache (enables bfcache) - Add three and @react-three/fiber to optimizePackageImports for better tree-shaking Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
104 lines
3.7 KiB
TypeScript
104 lines
3.7 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>({ 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"),
|
|
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"
|
|
>
|
|
{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.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({ 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({ 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>
|
|
);
|
|
}
|
|
|