Files
NexStudio/src/scripts/nx-cookie-consent.ts
Javaxman 16c380dfda Initial commit: sito NexStudio (Astro) con Cloudflare, CMS cookie e contenuti legali
- Home: hero, prodotti, servizi, stack, FAQ, CTA con form contatti inline (glossy) e particelle
- Header nero, menu con scroll-spy e alone su voce attiva, CTA Chattiamo (pre-chat)
- Pagine: privacy, cookie, GDPR, terms, about, codice etico, modello organizzativo, dove siamo, news
- Cookie consent first-party, chat gate, stampe-friendly su SubpageLayout
- Footer: Sezioni con Contattaci, Azienda con Dove siamo senza link per ora
- .gitignore: aggiunto .wrangler/ per stato locale Cloudflare

Made-with: Cursor
2026-04-22 13:07:40 +02:00

280 lines
8.0 KiB
TypeScript

type CategoryId = 'necessary' | 'preferences' | 'analytics' | 'marketing';
interface CcConfig {
storageKey: string;
policyVersion: string;
categories: readonly { id: CategoryId; required: boolean }[];
}
interface ConsentPayload {
policyVersion: string;
updatedAt: string;
categories: Record<CategoryId, boolean>;
}
const EVENT_NAME = 'nexstudio:cookie-consent';
const PANEL_ID = 'nx-cc-dialog';
/** Chrome può usare come `target` un nodo `#text` dentro al `<button>` */
function eventTargetElement(e: Event): Element | null {
const t = e.target;
if (t instanceof Element) return t;
if (t instanceof Text) return t.parentElement;
return null;
}
function panelRoot(): HTMLElement | null {
return document.getElementById(PANEL_ID);
}
function isPanelOpen(): boolean {
const el = panelRoot();
return Boolean(el && !el.hasAttribute('hidden'));
}
function closeConsentPanel(): void {
const el = panelRoot();
if (!el) return;
el.setAttribute('hidden', '');
el.setAttribute('aria-hidden', 'true');
document.body.classList.remove('overflow-hidden');
}
function openConsentPanel(): void {
const el = panelRoot();
if (!el) return;
el.removeAttribute('hidden');
el.setAttribute('aria-hidden', 'false');
document.body.classList.add('overflow-hidden');
}
function defaultCategories(cfg: CcConfig): Record<CategoryId, boolean> {
const out = {} as Record<CategoryId, boolean>;
for (const c of cfg.categories) {
out[c.id] = c.required ? true : false;
}
return out;
}
function parsePayload(raw: string | null, cfg: CcConfig): ConsentPayload | null {
if (!raw) return null;
try {
const v = JSON.parse(raw) as Partial<ConsentPayload>;
if (!v || typeof v !== 'object') return null;
if (v.policyVersion !== cfg.policyVersion) return null;
if (typeof v.updatedAt !== 'string') return null;
if (!v.categories || typeof v.categories !== 'object') return null;
const cats = { ...defaultCategories(cfg) };
for (const c of cfg.categories) {
if (typeof v.categories[c.id] === 'boolean') {
cats[c.id] = c.required ? true : Boolean(v.categories[c.id]);
}
}
return { policyVersion: cfg.policyVersion, updatedAt: v.updatedAt, categories: cats };
} catch {
return null;
}
}
function readConfig(): CcConfig {
const el = document.getElementById('nx-cc-model');
const raw = el?.textContent?.trim();
if (!raw) {
return {
storageKey: 'nexstudio.cookieConsent',
policyVersion: '1.0',
categories: [
{ id: 'necessary', required: true },
{ id: 'preferences', required: false },
{ id: 'analytics', required: false },
{ id: 'marketing', required: false },
],
};
}
return JSON.parse(raw) as CcConfig;
}
function save(cfg: CcConfig, categories: Record<CategoryId, boolean>): ConsentPayload {
const payload: ConsentPayload = {
policyVersion: cfg.policyVersion,
updatedAt: new Date().toISOString(),
categories: { ...categories },
};
for (const c of cfg.categories) {
if (c.required) payload.categories[c.id] = true;
}
try {
localStorage.setItem(cfg.storageKey, JSON.stringify(payload));
} catch {
/* ignore quota / private mode */
}
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: payload }));
return payload;
}
function getStored(cfg: CcConfig): ConsentPayload | null {
try {
return parsePayload(localStorage.getItem(cfg.storageKey), cfg);
} catch {
return null;
}
}
let nxCcDocCapture: ((e: MouseEvent) => void) | undefined;
function bind(): void {
const cfg = readConfig();
const banner = document.getElementById('nx-cc-banner');
const root = panelRoot();
if (!banner || !root) return;
closeConsentPanel();
function readForm(): Record<CategoryId, boolean> {
const d = panelRoot();
const base = defaultCategories(cfg);
base.preferences = Boolean(d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="preferences"]')?.checked);
base.analytics = Boolean(d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="analytics"]')?.checked);
base.marketing = Boolean(d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="marketing"]')?.checked);
return base;
}
function writeForm(cats: Record<CategoryId, boolean>) {
const d = panelRoot();
const p = d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="preferences"]');
const a = d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="analytics"]');
const m = d?.querySelector<HTMLInputElement>('[data-nx-cc-cat="marketing"]');
if (p) p.checked = cats.preferences;
if (a) a.checked = cats.analytics;
if (m) m.checked = cats.marketing;
}
function hideBanner() {
banner.setAttribute('hidden', '');
banner.setAttribute('aria-hidden', 'true');
}
function showBanner() {
banner.removeAttribute('hidden');
banner.setAttribute('aria-hidden', 'false');
}
function applyFromStorage() {
const stored = getStored(cfg);
if (stored) {
writeForm(stored.categories);
hideBanner();
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: stored }));
} else {
writeForm(defaultCategories(cfg));
showBanner();
}
closeConsentPanel();
}
function persistAndClose(cats: Record<CategoryId, boolean>) {
save(cfg, cats);
hideBanner();
closeConsentPanel();
}
banner.querySelector('[data-nx-cc-reject]')?.addEventListener('click', () => {
persistAndClose(defaultCategories(cfg));
});
banner.querySelector('[data-nx-cc-accept]')?.addEventListener('click', () => {
const cats = defaultCategories(cfg);
for (const c of cfg.categories) {
if (!c.required) cats[c.id] = true;
}
persistAndClose(cats);
});
banner.querySelector('[data-nx-cc-customize]')?.addEventListener('click', () => {
const stored = getStored(cfg);
writeForm(stored?.categories ?? defaultCategories(cfg));
openConsentPanel();
});
const onDocClickCapture = (e: MouseEvent) => {
if (!isPanelOpen()) return;
const d = panelRoot();
if (!d) return;
const el = eventTargetElement(e);
if (!el || !d.contains(el)) return;
if (el.closest('[data-nx-cc-save]')) {
e.preventDefault();
e.stopPropagation();
persistAndClose(readForm());
return;
}
if (el.closest('[data-nx-cc-cancel]')) {
e.preventDefault();
e.stopPropagation();
closeConsentPanel();
return;
}
if (!el.closest('[data-nx-cc-panel]')) {
e.preventDefault();
e.stopPropagation();
closeConsentPanel();
}
};
if (nxCcDocCapture) {
document.removeEventListener('click', nxCcDocCapture, true);
}
nxCcDocCapture = onDocClickCapture;
document.addEventListener('click', nxCcDocCapture, true);
document.querySelectorAll<HTMLElement>('[data-nx-cc-open]').forEach((el) => {
el.addEventListener('click', (e) => {
e.preventDefault();
const stored = getStored(cfg);
writeForm(stored?.categories ?? defaultCategories(cfg));
openConsentPanel();
});
});
applyFromStorage();
(window as unknown as { NexStudioCookieConsent?: unknown }).NexStudioCookieConsent = {
getConsent: () => getStored(cfg),
openPreferences: () => {
const stored = getStored(cfg);
writeForm(stored?.categories ?? defaultCategories(cfg));
openConsentPanel();
},
acceptAll: () => {
const cats = defaultCategories(cfg);
for (const c of cfg.categories) {
if (!c.required) cats[c.id] = true;
}
persistAndClose(cats);
},
rejectOptional: () => {
persistAndClose(defaultCategories(cfg));
},
};
}
if (typeof document !== 'undefined') {
window.addEventListener('pageshow', (e) => {
if (e.persisted) closeConsentPanel();
});
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
if (!isPanelOpen()) return;
e.preventDefault();
closeConsentPanel();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bind, { once: true });
} else {
bind();
}
}