### Overview Implement PDPA & GDPR compliant Cookie Consent Management Platform (CMP) for pps/website: - **Zustand Store (cookieConsentStore.ts):** Stores consent record ( ecessary, nalytics, marketing) in localStorage with skipHydration: true for Next.js SSR safety. - **Cookie Banner (CookieBanner.tsx):** Fixed bottom floating banner with Quiet Luxury styling, providing Accept All, Reject Non-Essential, and Customize buttons. - **Preferences Modal (CookiePreferencesModal.tsx):** Detailed modal dialog allowing users to granularly toggle Analytics and Marketing cookies (Strictly Necessary is locked). - **Script Manager (ScriptManager.tsx):** Coordinates Google Consent Mode v2 (nalytics_storage, d_storage), conditionally injects GA4, Meta Pixel, and TikTok Pixel, and dispatches homaroma:consent-change DOM events. - **Footer Revocation Link:** Added Cookie Preferences link in the Support column of Footer.tsx so users can modify/revoke consent anytime. - **Multilingual Support:** Added localized text for TH, EN, JA, and ZH. --------- Co-authored-by: Your Name <you@example.com> Reviewed-on: #14 Co-authored-by: antigravity-bot <bot@kittisak.com>
125 lines
3.0 KiB
TypeScript
125 lines
3.0 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
export const CURRENT_CONSENT_VERSION = '1.0';
|
|
|
|
export interface CookiePreferences {
|
|
necessary: boolean;
|
|
analytics: boolean;
|
|
marketing: boolean;
|
|
}
|
|
|
|
export interface CookieConsentRecord {
|
|
hasResponded: boolean;
|
|
version: string;
|
|
timestamp: string | null;
|
|
preferences: CookiePreferences;
|
|
}
|
|
|
|
interface CookieConsentState {
|
|
record: CookieConsentRecord;
|
|
isPreferencesOpen: boolean;
|
|
isHydrated: boolean;
|
|
|
|
// Actions
|
|
setHydrated: (hydrated: boolean) => void;
|
|
openPreferences: () => void;
|
|
closePreferences: () => void;
|
|
acceptAll: () => void;
|
|
rejectAll: () => void;
|
|
savePreferences: (preferences: Partial<CookiePreferences>) => void;
|
|
resetConsent: () => void;
|
|
}
|
|
|
|
const DEFAULT_RECORD: CookieConsentRecord = {
|
|
hasResponded: false,
|
|
version: CURRENT_CONSENT_VERSION,
|
|
timestamp: null,
|
|
preferences: {
|
|
necessary: true,
|
|
analytics: false,
|
|
marketing: false,
|
|
},
|
|
};
|
|
|
|
export const useCookieConsentStore = create<CookieConsentState>()(
|
|
persist(
|
|
(set) => ({
|
|
record: DEFAULT_RECORD,
|
|
isPreferencesOpen: false,
|
|
isHydrated: false,
|
|
|
|
setHydrated: (hydrated: boolean) => set({ isHydrated: hydrated }),
|
|
|
|
openPreferences: () => set({ isPreferencesOpen: true }),
|
|
closePreferences: () => set({ isPreferencesOpen: false }),
|
|
|
|
acceptAll: () => {
|
|
const now = new Date().toISOString();
|
|
set({
|
|
record: {
|
|
hasResponded: true,
|
|
version: CURRENT_CONSENT_VERSION,
|
|
timestamp: now,
|
|
preferences: {
|
|
necessary: true,
|
|
analytics: true,
|
|
marketing: true,
|
|
},
|
|
},
|
|
isPreferencesOpen: false,
|
|
});
|
|
},
|
|
|
|
rejectAll: () => {
|
|
const now = new Date().toISOString();
|
|
set({
|
|
record: {
|
|
hasResponded: true,
|
|
version: CURRENT_CONSENT_VERSION,
|
|
timestamp: now,
|
|
preferences: {
|
|
necessary: true,
|
|
analytics: false,
|
|
marketing: false,
|
|
},
|
|
},
|
|
isPreferencesOpen: false,
|
|
});
|
|
},
|
|
|
|
savePreferences: (userPrefs: Partial<CookiePreferences>) => {
|
|
const now = new Date().toISOString();
|
|
set((state) => ({
|
|
record: {
|
|
hasResponded: true,
|
|
version: CURRENT_CONSENT_VERSION,
|
|
timestamp: now,
|
|
preferences: {
|
|
necessary: true,
|
|
analytics: Boolean(userPrefs.analytics),
|
|
marketing: Boolean(userPrefs.marketing),
|
|
},
|
|
},
|
|
isPreferencesOpen: false,
|
|
}));
|
|
},
|
|
|
|
resetConsent: () => {
|
|
set({
|
|
record: DEFAULT_RECORD,
|
|
isPreferencesOpen: false,
|
|
});
|
|
},
|
|
}),
|
|
{
|
|
name: 'homaroma-cookie-consent',
|
|
partialize: (state) => ({ record: state.record }),
|
|
skipHydration: true,
|
|
onRehydrateStorage: () => (state) => {
|
|
state?.setHydrated(true);
|
|
},
|
|
}
|
|
)
|
|
);
|