601 lines
19 KiB
JavaScript
601 lines
19 KiB
JavaScript
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 qu’aprè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 d’audience est désactivée. Votre visite actuelle n’est 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();
|
||
}
|
||
});
|