Files
portfolio/app/components/ConsentProvider.tsx
denshooter 32e621df14
Some checks failed
Gitea CI / test-build (push) Failing after 5m21s
Dev Deployment (Zero Downtime) / deploy-dev (push) Successful in 18m29s
fix: namespace rate limit buckets per endpoint, remove custom analytics
- Add `prefix` param to checkRateLimit/getRateLimitHeaders so each endpoint
  has its own bucket (previously all shared `admin_${ip}`, causing 429s when
  analytics/track incremented past n8n endpoints' lower limits)
- n8n/hardcover/currently-reading → prefix 'n8n-reading'
- n8n/status → prefix 'n8n-status'
- analytics/track → prefix 'analytics-track'
- Remove custom analytics system (AnalyticsProvider, lib/analytics,
  lib/useWebVitals, all /api/analytics/* routes) — was causing 500s in
  production due to missing PostgreSQL PageView table
- Remove analytics consent toggle from ConsentBanner/ConsentProvider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:12:50 +01:00

86 lines
2.4 KiB
TypeScript

"use client";
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
export type ConsentState = {
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 {
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;
ready: boolean;
setConsent: (next: ConsentState) => void;
resetConsent: () => void;
}>({
consent: null,
ready: false,
setConsent: () => {},
resetConsent: () => {},
});
export function ConsentProvider({ children }: { children: React.ReactNode }) {
// IMPORTANT:
// Don't read `document.cookie` during SSR render (document is undefined), otherwise the
// server will render the banner while the client immediately hides it -> hydration mismatch.
// We resolve consent on the client after mount and only render the banner once `ready=true`.
const [consent, setConsentState] = useState<ConsentState | null>(null);
const [ready, setReady] = useState(false);
useEffect(() => {
setConsentState(readConsentFromCookie());
setReady(true);
}, []);
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, ready, setConsent, resetConsent }),
[consent, ready, setConsent, resetConsent],
);
return <ConsentContext.Provider value={value}>{children}</ConsentContext.Provider>;
}
export function useConsent() {
return useContext(ConsentContext);
}
export const consentCookieName = COOKIE_NAME;