1// Pages jeu et article prérendues (/[lang]/games|articles/<slug>) : sommaire actif, 2// apparitions, jackpot en direct, progression de lecture, partage, visionneuse photo. 3// Chargé après script.js : tout reste dans une IIFE pour ne pas entrer en 4// collision avec ses constantes globales (SUPABASE_URLâ¦). 5(() => { 6 const root = document.querySelector('.gp'); 7 if (!root) return; 8 9 const JACKPOT_ENDPOINT = 'https://hdholubjrnedvsvbezqw.supabase.co/rest/v1/jackpot?select=name,price,updated_at&order=updated_at.desc'; 10 const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhkaG9sdWJqcm5lZHZzdmJlenF3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjI5NTIzNDIsImV4cCI6MjA3ODUyODM0Mn0.b-DVyFmCAH-rHQLef0QdjS47zHEYCaIXDLabUfaUQDY'; 11 const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 12 13 // Hauteur réelle du header fixe â décalage des ancres et du sommaire collant 14 const syncHeaderOffset = () => { 15 const header = document.querySelector('.site-header'); 16 if (header) document.documentElement.style.setProperty('--gp-header', `${header.offsetHeight}px`); 17 }; 18 syncHeaderOffset(); 19 window.addEventListener('resize', syncHeaderOffset, { passive: true }); 20 window.addEventListener('load', syncHeaderOffset); 21 22 // Apparitions au scroll (le contenu reste visible sans JS) 23 if (!reduceMotion && 'IntersectionObserver' in window) { 24 root.classList.add('gp--anim'); 25 const io = new IntersectionObserver((entries) => { 26 entries.forEach((entry) => { 27 if (!entry.isIntersecting) return; 28 entry.target.classList.add('is-in'); 29 io.unobserve(entry.target); 30 }); 31 }, { rootMargin: '0px 0px -6% 0px', threshold: 0.05 }); 32 root.querySelectorAll('.gp-reveal').forEach((el) => io.observe(el)); 33 } 34 35 // Sommaire : section courante 36 const toc = root.querySelector('.gp-toc ol'); 37 const links = Array.from(root.querySelectorAll('.gp-toc a[href^="#"]')); 38 const targets = links 39 .map((link) => ({ link, section: document.getElementById(link.getAttribute('href').slice(1)) })) 40 .filter((entry) => entry.section); 41 let activeLink = null; 42 43 const setActive = (link) => { 44 if (link === activeLink) return; 45 activeLink = link; 46 links.forEach((a) => { 47 const on = a === link; 48 a.classList.toggle('is-active', on); 49 if (on) a.setAttribute('aria-current', 'location'); 50 else a.removeAttribute('aria-current'); 51 }); 52 // Barre horizontale (mobile) : garder l'entrée active visible 53 if (link && toc && toc.scrollWidth > toc.clientWidth) { 54 toc.scrollTo({ left: link.offsetLeft - 16, behavior: reduceMotion ? 'auto' : 'smooth' }); 55 } 56 }; 57 58 // Progression de lecture (articles) : du début à la fin du corps de l'article 59 const progressBar = document.querySelector('.ap-progress span'); 60 const articleBody = root.querySelector('.ap-body'); 61 const updateProgress = () => { 62 if (!progressBar || !articleBody) return; 63 const rect = articleBody.getBoundingClientRect(); 64 const span = Math.max(1, rect.height - window.innerHeight * 0.5); 65 const p = Math.min(1, Math.max(0, (window.innerHeight * 0.25 - rect.top) / span)); 66 progressBar.style.transform = `scaleX(${p})`; 67 }; 68 69 let ticking = false; 70 const updateActive = () => { 71 ticking = false; 72 updateProgress(); 73 const line = window.innerHeight * 0.35; 74 let current = null; 75 let currentTop = -Infinity; 76 targets.forEach(({ link, section }) => { 77 const top = section.getBoundingClientRect().top; 78 // Sections côte à côte (même hauteur) : on garde la première 79 if (top - line <= 0 && top > currentTop + 1) { 80 current = link; 81 currentTop = top; 82 } 83 }); 84 const atBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 4; 85 if (atBottom && targets.length) current = targets[targets.length - 1].link; 86 setActive(current); 87 }; 88 window.addEventListener('scroll', () => { 89 if (!ticking) { 90 ticking = true; 91 window.requestAnimationFrame(updateActive); 92 } 93 }, { passive: true }); 94 updateActive(); 95 96 // Règlement complet : s'ouvre quand on y arrive par une ancre 97 const rulebook = root.querySelector('.gp-rulebook'); 98 const openRulebookFromHash = () => { 99 if (rulebook && location.hash === '#rules') rulebook.open = true; 100 }; 101 window.addEventListener('hashchange', openRulebookFromHash); 102 openRulebookFromHash(); 103 104 // Partage (articles) : partage natif si disponible, sinon copie du lien 105 root.querySelectorAll('.ap-share').forEach((box) => { 106 const url = box.dataset.shareUrl || location.href; 107 const title = box.dataset.shareTitle || document.title; 108 const nativeBtn = box.querySelector('[data-share="native"]'); 109 if (nativeBtn && typeof navigator.share === 'function') { 110 nativeBtn.hidden = false;
111 nativeBtn.addEventListener('click', () => navigator.share({ title, url }).catch(() => {})); 112 } 113 const copyBtn = box.querySelector('[data-share="copy"]'); 114 if (!copyBtn) return; 115 copyBtn.addEventListener('click', async () => { 116 try { 117 await navigator.clipboard.writeText(url); 118 } catch (_) { 119 const field = document.createElement('textarea'); 120 field.value = url; 121 field.setAttribute('readonly', ''); 122 field.style.position = 'fixed'; 123 field.style.opacity = '0'; 124 document.body.appendChild(field); 125 field.select(); 126 document.execCommand('copy'); 127 field.remove(); 128 } 129 const label = copyBtn.querySelector('span'); 130 if (!label) return; 131 const initial = label.dataset.initial || label.textContent; 132 label.dataset.initial = initial; 133 label.textContent = box.dataset.copiedLabel || initial; 134 copyBtn.classList.add('is-done'); 135 window.setTimeout(() => { 136 label.textContent = initial; 137 copyBtn.classList.remove('is-done'); 138 }, 2200); 139 }); 140 }); 141 142 // Visionneuse photo (galerie d'article) ; sans JS, les liens ouvrent l'image 143 const gallery = root.querySelector('[data-lightbox]'); 144 if (gallery && typeof HTMLDialogElement === 'function') { 145 const items = Array.from(gallery.querySelectorAll('.ap-gallery__item')); 146 const icon = (d) => `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${d}"/></svg>`; 147 const dialog = document.createElement('dialog'); 148 dialog.className = 'ap-lightbox'; 149 dialog.innerHTML = ` 150 <figure class="ap-lightbox__figure"><img alt=""><figcaption class="ap-lightbox__count"></figcaption></figure> 151 <button type="button" class="ap-lightbox__btn ap-lightbox__close">${icon('M6 6l12 12M18 6L6 18')}</button> 152 <button type="button" class="ap-lightbox__btn ap-lightbox__prev">${icon('M15 6l-6 6 6 6')}</button> 153 <button type="button" class="ap-lightbox__btn ap-lightbox__next">${icon('M9 6l6 6-6 6')}</button>`; 154 document.body.appendChild(dialog); 155 const img = dialog.querySelector('img'); 156 const count = dialog.querySelector('.ap-lightbox__count'); 157 const closeBtn = dialog.querySelector('.ap-lightbox__close'); 158 const prevBtn = dialog.querySelector('.ap-lightbox__prev'); 159 const nextBtn = dialog.querySelector('.ap-lightbox__next'); 160 closeBtn.setAttribute('aria-label', gallery.dataset.labelClose || 'Fermer'); 161 prevBtn.setAttribute('aria-label', gallery.dataset.labelPrev || 'Précédente'); 162 nextBtn.setAttribute('aria-label', gallery.dataset.labelNext || 'Suivante'); 163 if (items.length < 2) { 164 prevBtn.hidden = true; 165 nextBtn.hidden = true; 166 } 167 168 let index = 0; 169 const show = (i) => { 170 index = (i + items.length) % items.length; 171 const item = items[index]; 172 img.src = item.getAttribute('href'); 173 img.alt = item.querySelector('img')?.alt || ''; 174 count.textContent = items.length > 1 ? `${index + 1} / ${items.length}` : ''; 175 }; 176 177 items.forEach((item, i) => { 178 item.addEventListener('click', (event) => { 179 event.preventDefault(); 180 show(i); 181 dialog.showModal(); 182 }); 183 }); 184 closeBtn.addEventListener('click', () => dialog.close()); 185 prevBtn.addEventListener('click', () => show(index - 1)); 186 nextBtn.addEventListener('click', () => show(index + 1)); 187 let swiped = false; 188 dialog.addEventListener('click', (event) => { 189 if (swiped) { swiped = false; return; } 190 if (event.target === dialog) dialog.close(); 191 }); 192 dialog.addEventListener('keydown', (event) => { 193 if (event.key === 'ArrowLeft') show(index - 1); 194 if (event.key === 'ArrowRight') show(index + 1); 195 }); 196 197 // Balayage horizontal sur mobile 198 let startX = null; 199 dialog.addEventListener('pointerdown', (event) => { startX = event.clientX; }); 200 dialog.addEventListener('pointerup', (event) => { 201 if (startX === null || items.length < 2) return; 202 const dx = event.clientX - startX; 203 startX = null; 204 if (Math.abs(dx) > 50) { 205 swiped = true; 206 show(index + (dx < 0 ? 1 : -1)); 207 } 208 }); 209 } 210 211 // Jackpot : valeur prérendue au build, rafraîchie ici 212 const jackpot = root.querySelector('[data-jackpot]'); 213 if (jackpot && window.fetch) {
214 const key = jackpot.dataset.jackpot; 215 const locale = jackpot.dataset.locale || 'fr-FR'; 216 const amountEl = jackpot.querySelector('[data-jackpot-amount]'); 217 const dateEl = jackpot.querySelector('[data-jackpot-date]'); 218 const normalize = (name) => String(name || '').normalize('NFD').replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); 219 const money = new Intl.NumberFormat(locale, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }); 220 221 const animateTo = (from, to) => { 222 if (reduceMotion || !Number.isFinite(from) || from === to) { 223 amountEl.textContent = money.format(to); 224 return; 225 } 226 const start = performance.now(); 227 const duration = 1100; 228 const step = (now) => { 229 const p = Math.min(1, (now - start) / duration); 230 const eased = 1 - Math.pow(1 - p, 3); 231 amountEl.textContent = money.format(from + (to - from) * eased); 232 if (p < 1) window.requestAnimationFrame(step); 233 }; 234 window.requestAnimationFrame(step); 235 }; 236 237 fetch(JACKPOT_ENDPOINT, { headers: { apikey: ANON_KEY, Authorization: `Bearer ${ANON_KEY}` } }) 238 .then((res) => (res.ok ? res.json() : [])) 239 .then((rows) => { 240 const row = Array.isArray(rows) ? rows.find((r) => normalize(r.name) === key) : null; 241 if (!row || !amountEl) return; 242 const amount = Number(String(row.price ?? '').replace(/[^\d,.-]/g, '').replace(',', '.')); 243 if (!Number.isFinite(amount)) return; 244 animateTo(Number(amountEl.dataset.jackpotAmount), amount); 245 amountEl.dataset.jackpotAmount = String(amount); 246 const updated = new Date(row.updated_at); 247 if (dateEl && !Number.isNaN(updated.getTime())) { 248 const label = jackpot.dataset.updatedLabel || ''; 249 dateEl.textContent = `${label} ${updated.toLocaleDateString(locale, { day: 'numeric', month: 'long', timeZone: 'Europe/Paris' })}`; 250 } 251 }) 252 .catch(() => {}); 253 } 254})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.