PageSourceSearch

https://www.pariselyseesclub.com/game-page.js?v=1791332800

js pariselyseesclub.com collected 2026-10-08 03:47:30 UTC 11,123 bytes, 254 lines download raw bytes

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.