Offre de printemps: -10% sur votre 1er cours. Se termine dans --:--:--

Catalogue des cours

Tags
') ]); document.getElementById('site-header').innerHTML = h; document.getElementById('site-footer').innerHTML = f; initUIBindings(); initCookieConsent(); } function initUIBindings() { document.addEventListener('click', (e) => { const open = e.target.closest('[data-open]'); const close = e.target.closest('[data-close]'); if (open) { const id = open.getAttribute('data-open'); const m = document.getElementById(id); if (m) { m.classList.remove('hidden'); m.setAttribute('aria-hidden','false'); } } if (close) { const id = close.getAttribute('data-close'); const m = document.getElementById(id); if (m) { m.classList.add('hidden'); m.setAttribute('aria-hidden','true'); } } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('#modal-course,#modal-msg,#modal-cookies,#modal-theme').forEach(m => { if (!m.classList.contains('hidden')) { m.classList.add('hidden'); m.setAttribute('aria-hidden','true'); } }); } }); document.addEventListener('click', (e) => { const modals = ['modal-course','modal-msg','modal-cookies','modal-theme']; const overlay = modals.find(id => e.target === document.getElementById(id)); if (overlay) { const m = document.getElementById(overlay); m.classList.add('hidden'); m.setAttribute('aria-hidden','true'); } }); const storedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.classList.toggle('dark', storedTheme === 'dark'); const themeBtn = document.querySelector('[data-action="toggle-theme"]'); if (themeBtn) { themeBtn.addEventListener('click', () => { const current = localStorage.getItem('theme') || 'light'; const next = current === 'light' ? 'dark' : 'light'; localStorage.setItem('theme', next); document.documentElement.classList.toggle('dark', next === 'dark'); const confirm = document.getElementById('modal-theme'); if (confirm) { confirm.classList.remove('hidden'); confirm.setAttribute('aria-hidden','false'); } }); } } function initCookieConsent() { const key = 'cookies-accepted'; const accepted = localStorage.getItem(key); const m = document.getElementById('modal-cookies'); if (m && !accepted) { m.classList.remove('hidden'); m.setAttribute('aria-hidden','false'); const btn = m.querySelector('[data-accept-cookies]'); if (btn) btn.addEventListener('click', () => { localStorage.setItem(key,'1'); m.classList.add('hidden'); m.setAttribute('aria-hidden','true'); }); } } const state = { items: [], page: 1, perPage: 9, filters: {} }; function loadFiltersFromForm() { const form = document.getElementById('filters'); const fd = new FormData(form); const q = (fd.get('q') || '').toString().trim().toLowerCase(); const level = fd.get('level') || ''; const minPriceRaw = fd.get('minPrice'); const maxPriceRaw = fd.get('maxPrice'); const minPrice = minPriceRaw === null || minPriceRaw === '' ? 0 : Math.max(0, parseFloat(minPriceRaw)); const maxPrice = maxPriceRaw === null || maxPriceRaw === '' ? Number.POSITIVE_INFINITY : Math.max(0, parseFloat(maxPriceRaw)); const sort = fd.get('sort') || 'reco'; const tags = fd.getAll('tag'); state.filters = { q, level, minPrice, maxPrice, sort, tags }; state.page = 1; } function applyFilters() { let arr = [...state.items]; const { q, level, minPrice, maxPrice, tags, sort } = state.filters; if (q) { arr = arr.filter(it => it.title.toLowerCase().includes(q) || it.short.toLowerCase().includes(q) || it.details.toLowerCase().includes(q) || it.tags.some(t => t.toLowerCase().includes(q)) ); } if (level) arr = arr.filter(it => it.level === level); arr = arr.filter(it => it.priceEUR >= minPrice && it.priceEUR <= maxPrice); if (tags && tags.length) { arr = arr.filter(it => tags.every(tag => it.tags.includes(tag))); } switch (sort) { case 'price-asc': arr.sort((a,b)=>a.priceEUR-b.priceEUR); break; case 'price-desc': arr.sort((a,b)=>b.priceEUR-a.priceEUR); break; case 'duration-asc': arr.sort((a,b)=>a.durationHours-b.durationHours); break; case 'duration-desc': arr.sort((a,b)=>b.durationHours-a.durationHours); break; case 'rating-desc': arr.sort((a,b)=>b.rating-a.rating); break; default: arr.sort((a,b)=>b.rating-a.rating || a.priceEUR-b.priceEUR); } return arr; } function paginate(arr) { const start = (state.page - 1) * state.perPage; return { slice: arr.slice(start, start + state.perPage), totalPages: Math.max(1, Math.ceil(arr.length / state.perPage)), total: arr.length }; } function getFavorites() { try { return JSON.parse(localStorage.getItem('favorites')||'[]'); } catch { return []; } } function setFavorites(arr) { localStorage.setItem('favorites', JSON.stringify([...new Set(arr)])); } function toggleFavorite(id) { const favs = getFavorites(); const has = favs.includes(id); const next = has ? favs.filter(x=>x!==id) : favs.concat(id); setFavorites(next); showMsg(has ? 'Retiré des favoris.' : 'Ajouté aux favoris !'); renderList(); } function getCart() { try { return JSON.parse(localStorage.getItem('cart')||'{}'); } catch { return {}; } } function setCart(obj) { localStorage.setItem('cart', JSON.stringify(obj)); } function addToCart(id, qty=1) { const item = state.items.find(x=>x.id===id); if (!item) return; if (item.spots <= 0) { showMsg('Ce cours est complet.'); return; } const cart = getCart(); cart[id] = (cart[id]||0) + qty; setCart(cart); showMsg('Ajouté au panier.'); } function showMsg(text) { const m = document.getElementById('modal-msg'); document.getElementById('modal-msg-text').textContent = text; m.classList.remove('hidden'); m.setAttribute('aria-hidden','false'); } function renderList() { const list = document.getElementById('list'); const empty = document.getElementById('empty'); const favs = getFavorites(); const filtered = applyFilters(); const { slice, totalPages, total } = paginate(filtered); list.innerHTML = ''; slice.forEach(it => { const isFav = favs.includes(it.id); const soldOut = it.spots <= 0; const li = document.createElement('li'); li.className = 'p-5 border rounded-lg flex flex-col gap-2 bg-white dark:bg-slate-900 dark:border-white/10 hover:shadow-brand transition-shadow duration-200 s3d4f'; li.innerHTML = `

${it.title}

${it.level}

${it.short}

Durée: ${it.durationHours} h • Note: ${it.rating.toFixed(1)} • Places: ${it.spots}
${it.priceEUR.toFixed(2)} €
${it.tags.map(t=>`${t}`).join('')}
`; list.appendChild(li); }); document.getElementById('pageInfo').textContent = `Page ${state.page} / ${totalPages} — ${total} résultat(s)`; const prev = document.getElementById('prevPage'); const next = document.getElementById('nextPage'); prev.disabled = state.page <= 1; next.disabled = state.page >= totalPages; empty.classList.toggle('hidden', total > 0); } function openCourseModal(it) { document.getElementById('modal-title').textContent = it.title; document.getElementById('modal-short').textContent = it.short; document.getElementById('modal-details').textContent = it.details; document.getElementById('modal-meta').textContent = `Niveau: ${it.level} • Durée: ${it.durationHours} h • Note: ${it.rating.toFixed(1)} • Prix: ${it.priceEUR.toFixed(2)} € • Places restantes: ${it.spots}`; const ul = document.getElementById('modal-syllabus'); ul.innerHTML = it.syllabus.map(s=>`
  • ${s}
  • `).join(''); const favBtn = document.getElementById('modal-add-fav'); const cartBtn = document.getElementById('modal-add-cart'); favBtn.onclick = () => toggleFavorite(it.id); cartBtn.onclick = () => addToCart(it.id); const m = document.getElementById('modal-course'); m.classList.remove('hidden'); m.setAttribute('aria-hidden','false'); } function validateFilters() { const min = document.getElementById('minPrice').value; const max = document.getElementById('maxPrice').value; if (min && max && parseFloat(min) > parseFloat(max)) { showMsg('Le prix minimum ne peut pas dépasser le prix maximum.'); return false; } return true; } document.getElementById('filters').addEventListener('submit', (e) => { e.preventDefault(); if (!validateFilters()) return; loadFiltersFromForm(); renderList(); }); document.getElementById('resetFilters').addEventListener('click', () => { const form = document.getElementById('filters'); form.reset(); loadFiltersFromForm(); renderList(); }); document.getElementById('prevPage').addEventListener('click', () => { state.page--; renderList(); }); document.getElementById('nextPage').addEventListener('click', () => { state.page++; renderList(); }); document.addEventListener('click', (e) => { const btn = e.target.closest('[data-action]'); if (!btn) return; const idAttr = btn.getAttribute('data-id'); const id = idAttr ? Number(idAttr) : null; const action = btn.getAttribute('data-action'); const item = id !== null ? state.items.find(x=>x.id===id) : null; if (action === 'details' && item) openCourseModal(item); if (action === 'fav' && id !== null) toggleFavorite(id); if (action === 'addcart' && id !== null) addToCart(id); }); function debounce(fn, wait) { let t; return (...args)=>{ clearTimeout(t); t=setTimeout(()=>fn(...args), wait); }; } const qInput = document.getElementById('q'); qInput.addEventListener('input', debounce(() => { loadFiltersFromForm(); renderList(); }, 250)); function initCountdown() { const key = 'promo-ends'; let end = localStorage.getItem(key); if (!end) { const now = Date.now(); end = now + 72*3600*1000; localStorage.setItem(key, String(end)); } else { end = parseInt(end, 10); } const el = document.getElementById('deal-timer'); function tick() { const diff = end - Date.now(); if (diff <= 0) { el.textContent = 'terminée'; const bar = el.closest('.z9y8x'); if (bar) bar.classList.add('hidden'); return; } const h = Math.floor(diff/3600000); const m = Math.floor((diff%3600000)/60000); const s = Math.floor((diff%60000)/1000); el.textContent = `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; requestAnimationFrame(()=>setTimeout(tick, 200)); } tick(); } async function init() { await injectFragments(); initCountdown(); try { const res = await fetch('catalog.json', { headers: {'Accept':'application/json'} }); if (!res.ok) throw new Error('HTTP '+res.status); const items = await res.json(); state.items = Array.isArray(items) ? items : []; } catch (e) { state.items = []; showMsg("Impossible de charger le catalogue. Veuillez réessayer plus tard."); } loadFiltersFromForm(); renderList(); } init();