2026-05-02 09:04:12 +00:00

601 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

document.addEventListener('DOMContentLoaded', () => {
const COOKIE_NAME = 'ptcs_consent';
const VISIT_COOKIE_NAME = 'ptcs_visit_id';
const COOKIE_MAX_AGE = 60 * 60 * 24 * 180;
const VISIT_REFRESH_MS = 30 * 1000;
const STORAGE_KEYS = {
personalization: 'ptcs_ui_preferences'
};
const banner = document.getElementById('cookie-banner');
const overlay = document.getElementById('cookie-overlay');
const reopenButton = document.getElementById('cookie-reopen');
const floatingReminder = reopenButton ? reopenButton.closest('.cookie-floating') : null;
const reopenLabel = document.getElementById('cookie-reopen-label');
const footerCookieSettings = document.getElementById('footer-cookie-settings');
const dismissScrollHint = document.getElementById('scroll-hint-dismiss');
const scrollHint = document.getElementById('scroll-hint');
const toastStack = document.getElementById('toast-stack');
const personalizationControls = Array.from(document.querySelectorAll('[data-consent-control="personalization"]'));
const audienceControls = Array.from(document.querySelectorAll('[data-consent-control="audience"]'));
const consentActionButtons = Array.from(document.querySelectorAll('[data-cookie-action]'));
const isHomePage = document.body?.dataset.page === 'home';
const bodyStartsLocked = document.body?.dataset.cookieLock === 'pending';
const visitCounterRoot = document.querySelector('[data-visit-counter-endpoint]');
const visitCounterEndpoint = visitCounterRoot?.dataset.visitCounterEndpoint || '/api/visit-counter.php';
const liveWindowMinutes = Number(visitCounterRoot?.dataset.liveWindowMinutes || 5);
const visitStatusNodes = Array.from(document.querySelectorAll('[data-visit-status]'));
const visitLiveNodes = Array.from(document.querySelectorAll('[data-visit-live]'));
const visitDailyNodes = Array.from(document.querySelectorAll('[data-visit-daily]'));
const visitTotalNodes = Array.from(document.querySelectorAll('[data-visit-total]'));
const visitUpdatedNodes = Array.from(document.querySelectorAll('[data-visit-updated]'));
const numberFormatter = new Intl.NumberFormat('fr-FR');
const state = {
prefs: {
essential: true,
personalization: false,
audience: false,
timestamp: null
},
hasSavedDecision: false,
requiresBlockingChoice: bodyStartsLocked,
visitCounts: visitCounterRoot ? {
live: Number(visitCounterRoot.dataset.initialLive || 0),
daily: Number(visitCounterRoot.dataset.initialDaily || 0),
total: Number(visitCounterRoot.dataset.initialTotal || 0),
updated_at: visitCounterRoot.dataset.updatedAt || null,
updated_label: visitCounterRoot.dataset.updatedLabel || ''
} : null,
visitRefreshTimer: null
};
function safeParse(rawValue) {
if (!rawValue) {
return null;
}
try {
return JSON.parse(rawValue);
} catch (error) {
return null;
}
}
function storageGet(key) {
try {
return window.localStorage.getItem(key);
} catch (error) {
return null;
}
}
function storageSet(key, value) {
try {
window.localStorage.setItem(key, value);
} catch (error) {
// no-op
}
}
function storageRemove(key) {
try {
window.localStorage.removeItem(key);
} catch (error) {
// no-op
}
}
function getCookie(name) {
const prefix = `${name}=`;
const cookies = document.cookie ? document.cookie.split('; ') : [];
for (const row of cookies) {
if (row.startsWith(prefix)) {
return decodeURIComponent(row.substring(prefix.length));
}
}
return null;
}
function setCookie(name, value, maxAge) {
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
}
function deleteCookie(name) {
document.cookie = `${name}=; path=/; max-age=0; SameSite=Lax`;
}
function normalizePrefs(rawPrefs = {}) {
return {
essential: true,
personalization: Boolean(rawPrefs.personalization),
audience: Boolean(rawPrefs.audience),
timestamp: rawPrefs.timestamp || null
};
}
function readSavedPrefs() {
const parsed = safeParse(getCookie(COOKIE_NAME));
return parsed ? normalizePrefs(parsed) : null;
}
function setControlState(controls, value) {
controls.forEach((control) => {
control.checked = Boolean(value);
});
}
function setBadge(key, label, status) {
document.querySelectorAll(`[data-consent-badge="${key}"]`).forEach((badge) => {
badge.textContent = label;
badge.dataset.state = status;
});
}
function formatNumber(value) {
const numericValue = Number(value);
return numberFormatter.format(Number.isFinite(numericValue) ? numericValue : 0);
}
function formatTime(dateString) {
if (!dateString) {
return '—';
}
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) {
return '—';
}
return new Intl.DateTimeFormat('fr-FR', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
}
function updateSummaryText() {
let summary = "Le site conserve uniquement l'essentiel tant que vous n'avez pas choisi d'options supplémentaires.";
if (state.prefs.personalization && state.prefs.audience) {
summary = 'Réglages actifs : essentiels, personnalisation et compteur visiteurs anonymisé.';
} else if (state.prefs.personalization) {
summary = 'Réglages actifs : essentiels et personnalisation. Le compteur visiteurs reste inactif.';
} else if (state.prefs.audience) {
summary = "Réglages actifs : essentiels et compteur visiteurs anonymisé. Les préférences d'interface restent minimales.";
}
document.querySelectorAll('[data-consent-summary]').forEach((node) => {
node.textContent = summary;
});
}
function updateReminderLabel() {
if (!reopenLabel) {
return;
}
if (state.prefs.personalization && state.prefs.audience) {
reopenLabel.textContent = 'Cookies : choix personnalisés';
return;
}
if (state.prefs.personalization || state.prefs.audience) {
reopenLabel.textContent = 'Cookies : options partielles';
return;
}
reopenLabel.textContent = 'Cookies : essentiels';
}
function focusBannerPrimaryAction() {
if (!banner) {
return;
}
const primaryAction = banner.querySelector('[data-cookie-action="reject"]')
|| banner.querySelector('[data-cookie-action="save"]')
|| banner.querySelector('button, [href], input:not([disabled])');
if (primaryAction instanceof HTMLElement) {
primaryAction.focus({ preventScroll: true });
return;
}
if (banner instanceof HTMLElement) {
banner.focus({ preventScroll: true });
}
}
function syncConsentBarrier() {
const shouldLock = isHomePage && state.requiresBlockingChoice && !state.hasSavedDecision;
document.body.classList.toggle('cookie-consent-locked', shouldLock);
document.body.dataset.cookieLock = shouldLock ? 'pending' : 'released';
if (overlay) {
overlay.hidden = !shouldLock;
}
if (banner) {
banner.classList.toggle('cookie-banner--modal', shouldLock);
banner.setAttribute('aria-modal', shouldLock ? 'true' : 'false');
}
const shouldHideReminder = !state.hasSavedDecision && banner && !banner.hidden;
if (floatingReminder) {
floatingReminder.hidden = shouldHideReminder;
}
if (reopenButton) {
reopenButton.hidden = false;
if (shouldHideReminder) {
reopenButton.setAttribute('aria-hidden', 'true');
} else {
reopenButton.removeAttribute('aria-hidden');
}
}
}
function refreshPersonalizationUI() {
const stored = safeParse(storageGet(STORAGE_KEYS.personalization)) || {};
if (!scrollHint) {
return;
}
if (!state.prefs.personalization) {
scrollHint.hidden = false;
return;
}
scrollHint.hidden = stored.scrollHintDismissed === true;
}
function showToast(message) {
if (!toastStack) {
return;
}
const toast = document.createElement('div');
toast.className = 'app-toast';
toast.setAttribute('role', 'status');
toast.textContent = message;
toastStack.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.add('is-visible');
});
window.setTimeout(() => {
toast.classList.remove('is-visible');
window.setTimeout(() => {
toast.remove();
}, 220);
}, 3200);
}
function updateVisitCounter(snapshot = null) {
if (!visitCounterRoot) {
return;
}
if (snapshot && typeof snapshot === 'object') {
state.visitCounts = {
live: Number(snapshot.live || 0),
daily: Number(snapshot.daily || 0),
total: Number(snapshot.total || 0),
updated_at: snapshot.updated_at || null,
updated_label: snapshot.updated_label || ''
};
}
const counts = state.visitCounts || {
live: 0,
daily: 0,
total: 0,
updated_at: null,
updated_label: ''
};
visitLiveNodes.forEach((node) => {
node.textContent = formatNumber(counts.live);
});
visitDailyNodes.forEach((node) => {
node.textContent = formatNumber(counts.daily);
});
visitTotalNodes.forEach((node) => {
node.textContent = formatNumber(counts.total);
});
let statusText = `Le compteur affiche l'audience mesurée sur le site. “En direct” correspond aux visiteurs actifs sur les ${liveWindowMinutes} dernières minutes.`;
if (!state.hasSavedDecision) {
statusText = "Le compteur est visible, mais votre visite ne sera comptée quaprès votre choix cookies.";
} else if (state.prefs.audience) {
statusText = "Le compteur anonyme est actif pour votre visite. Les chiffres se mettent à jour automatiquement sans service tiers.";
} else {
statusText = "Le compteur reste en lecture seule tant que la mesure locale daudience est désactivée. Votre visite actuelle nest pas ajoutée.";
}
visitStatusNodes.forEach((node) => {
node.textContent = statusText;
});
const timeLabel = counts.updated_label || formatTime(counts.updated_at);
const updatedText = `Dernière mise à jour : ${timeLabel} · “En direct” = visiteurs actifs sur les ${liveWindowMinutes} dernières minutes.`;
visitUpdatedNodes.forEach((node) => {
node.textContent = updatedText;
});
}
async function requestVisitCounter(method = 'GET', payload = null) {
if (!visitCounterRoot) {
return null;
}
const options = {
method,
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin',
cache: 'no-store'
};
if (payload !== null) {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(payload);
}
try {
const response = await fetch(visitCounterEndpoint, options);
if (!response.ok) {
return null;
}
const data = await response.json();
return data && data.success ? data : null;
} catch (error) {
return null;
}
}
function clearVisitRefreshTimer() {
if (state.visitRefreshTimer) {
window.clearInterval(state.visitRefreshTimer);
state.visitRefreshTimer = null;
}
}
function startVisitRefreshTimer() {
if (!visitCounterRoot || state.visitRefreshTimer) {
return;
}
state.visitRefreshTimer = window.setInterval(() => {
if (document.visibilityState === 'hidden') {
return;
}
if (state.prefs.audience) {
void pingVisitCounter();
return;
}
void refreshVisitSnapshot();
}, VISIT_REFRESH_MS);
}
async function refreshVisitSnapshot() {
const data = await requestVisitCounter('GET');
if (data && data.counts) {
updateVisitCounter(data.counts);
return;
}
updateVisitCounter();
}
async function pingVisitCounter() {
const token = getCookie(VISIT_COOKIE_NAME);
const data = await requestVisitCounter('POST', {
action: 'ping',
token
});
if (data?.token) {
setCookie(VISIT_COOKIE_NAME, data.token, COOKIE_MAX_AGE);
}
if (data?.counts) {
updateVisitCounter(data.counts);
return;
}
updateVisitCounter();
}
async function forgetVisitCounter() {
const token = getCookie(VISIT_COOKIE_NAME);
deleteCookie(VISIT_COOKIE_NAME);
if (!token) {
await refreshVisitSnapshot();
return;
}
const data = await requestVisitCounter('POST', {
action: 'forget',
token
});
if (data?.counts) {
updateVisitCounter(data.counts);
return;
}
await refreshVisitSnapshot();
}
function syncVisitCounter(previousAudienceEnabled = false) {
if (!visitCounterRoot) {
return;
}
clearVisitRefreshTimer();
if (state.prefs.audience) {
void pingVisitCounter().finally(() => {
startVisitRefreshTimer();
});
return;
}
if (previousAudienceEnabled) {
void forgetVisitCounter().finally(() => {
startVisitRefreshTimer();
});
return;
}
deleteCookie(VISIT_COOKIE_NAME);
void refreshVisitSnapshot().finally(() => {
startVisitRefreshTimer();
});
}
function applyPrefs(prefs) {
const previousAudienceEnabled = Boolean(state.prefs.audience);
state.prefs = normalizePrefs(prefs);
document.body.dataset.consentPersonalization = state.prefs.personalization ? 'on' : 'off';
document.body.dataset.consentAudience = state.prefs.audience ? 'on' : 'off';
setControlState(personalizationControls, state.prefs.personalization);
setControlState(audienceControls, state.prefs.audience);
setBadge('essential', 'Toujours actif', 'locked');
setBadge('personalization', state.prefs.personalization ? 'Activée' : 'Désactivée', state.prefs.personalization ? 'on' : 'off');
setBadge('audience', state.prefs.audience ? 'Activée' : 'Désactivée', state.prefs.audience ? 'on' : 'off');
if (!state.prefs.personalization) {
storageRemove(STORAGE_KEYS.personalization);
}
refreshPersonalizationUI();
updateSummaryText();
updateReminderLabel();
updateVisitCounter();
syncVisitCounter(previousAudienceEnabled);
}
function openBanner() {
if (!banner) {
return;
}
banner.hidden = false;
syncConsentBarrier();
if (reopenButton) {
reopenButton.setAttribute('aria-expanded', 'true');
}
focusBannerPrimaryAction();
}
function closeBanner() {
if (!banner) {
return;
}
banner.hidden = true;
syncConsentBarrier();
if (reopenButton) {
reopenButton.setAttribute('aria-expanded', 'false');
}
}
function savePrefs(nextPrefs, notice) {
const normalized = normalizePrefs(nextPrefs);
normalized.timestamp = new Date().toISOString();
setCookie(COOKIE_NAME, JSON.stringify(normalized), COOKIE_MAX_AGE);
state.hasSavedDecision = true;
state.requiresBlockingChoice = false;
applyPrefs(normalized);
closeBanner();
showToast(notice);
}
if (dismissScrollHint && scrollHint) {
dismissScrollHint.addEventListener('click', () => {
scrollHint.hidden = true;
if (state.prefs.personalization) {
storageSet(STORAGE_KEYS.personalization, JSON.stringify({
scrollHintDismissed: true,
updatedAt: new Date().toISOString()
}));
showToast('Le rappel mobile a été masqué pour cet appareil.');
}
});
}
consentActionButtons.forEach((button) => {
button.addEventListener('click', () => {
const action = button.dataset.cookieAction;
if (action === 'accept') {
savePrefs({
essential: true,
personalization: true,
audience: true
}, 'Toutes les options facultatives ont été activées.');
return;
}
if (action === 'reject') {
savePrefs({
essential: true,
personalization: false,
audience: false
}, 'Seuls les cookies essentiels sont conservés.');
return;
}
if (action === 'save') {
savePrefs({
essential: true,
personalization: personalizationControls.some((control) => control.checked),
audience: audienceControls.some((control) => control.checked)
}, 'Vos préférences cookies ont été enregistrées.');
}
});
});
[reopenButton, footerCookieSettings].forEach((trigger) => {
if (!trigger) {
return;
}
trigger.addEventListener('click', () => {
openBanner();
});
});
document.addEventListener('visibilitychange', () => {
if (!visitCounterRoot || document.visibilityState !== 'visible') {
return;
}
if (state.prefs.audience) {
void pingVisitCounter();
return;
}
void refreshVisitSnapshot();
});
const savedPrefs = readSavedPrefs();
state.hasSavedDecision = Boolean(savedPrefs);
state.requiresBlockingChoice = isHomePage && !savedPrefs;
if (savedPrefs) {
applyPrefs(savedPrefs);
closeBanner();
} else {
applyPrefs({ essential: true, personalization: false, audience: false });
openBanner();
}
});