Thème mis à jour
Vos préférences d’affichage ont été enregistrées.
Offre de printemps: -10% sur votre 1er cours. Se termine dans
--:--:--
Aucun cours ne correspond à votre recherche.
Détails du cours
Gestion des cookies
Nous utilisons des cookies pour améliorer votre expérience. En acceptant, vous consentez à l'utilisation des cookies essentiels et analytiques.
Thème mis à jour
Vos préférences de thème ont été enregistrées.
')
]);
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();