feat(website): implement PDPA cookie consent banner and script manager (#14)
### 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>
This commit was merged in pull request #14.
This commit is contained in:
1 parent
9a18463bda
commit
b27afb4561
11 files changed
+692
-7
No files matched your search
@@ -0,0 +1,124 @@
|
||||
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);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in new issue
Block a user