Files
portfolio/app/components/ConsentProvider.tsx
Cursor Agent fbce838d3f fix(consent): avoid banner flashing on reload
Initialize consent state from cookie synchronously so the banner only shows when no choice was made.

fix(api): fail-soft when DB schema missing

Return null/empty content for CMS endpoints when migrations are not applied instead of crashing with Prisma P2021/P2022.

fix(n8n): parse status response defensively

Handle empty/invalid JSON bodies from n8n to prevent activity feed from getting stuck.

Co-authored-by: dennis <dennis@konkol.net>
2026-01-14 21:47:31 +00:00

77 lines
2.1 KiB
TypeScript

"use client";
import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
export type ConsentState = {
analytics: boolean;
chat: boolean;
};
const COOKIE_NAME = "dk0_consent_v1";
function readConsentFromCookie(): ConsentState | null {
if (typeof document === "undefined") return null;
const match = document.cookie
.split(";")
.map((c) => c.trim())
.find((c) => c.startsWith(`${COOKIE_NAME}=`));
if (!match) return null;
const value = decodeURIComponent(match.split("=").slice(1).join("="));
try {
const parsed = JSON.parse(value) as Partial<ConsentState>;
return {
analytics: !!parsed.analytics,
chat: !!parsed.chat,
};
} catch {
return null;
}
}
function writeConsentCookie(value: ConsentState) {
const encoded = encodeURIComponent(JSON.stringify(value));
// 180 days
const maxAge = 60 * 60 * 24 * 180;
document.cookie = `${COOKIE_NAME}=${encoded}; path=/; max-age=${maxAge}; samesite=lax`;
}
const ConsentContext = createContext<{
consent: ConsentState | null;
setConsent: (next: ConsentState) => void;
resetConsent: () => void;
}>({
consent: null,
setConsent: () => {},
resetConsent: () => {},
});
export function ConsentProvider({ children }: { children: React.ReactNode }) {
// Read cookie synchronously so we don't flash the banner on every reload.
const [consent, setConsentState] = useState<ConsentState | null>(() => readConsentFromCookie());
const setConsent = useCallback((next: ConsentState) => {
setConsentState(next);
writeConsentCookie(next);
}, []);
const resetConsent = useCallback(() => {
setConsentState(null);
// expire cookie
document.cookie = `${COOKIE_NAME}=; path=/; max-age=0; samesite=lax`;
}, []);
const value = useMemo(
() => ({ consent, setConsent, resetConsent }),
[consent, setConsent, resetConsent],
);
return <ConsentContext.Provider value={value}>{children}</ConsentContext.Provider>;
}
export function useConsent() {
return useContext(ConsentContext);
}
export const consentCookieName = COOKIE_NAME;