Catalogus

Gebruik filters of typ “/” om direct te zoeken.

Zoeken

Categorie:
Niveau:
Duur: min
Prijs:
Rooster:

    Je mandje

      0 items
      Totaal: € 0.00
      ') ]); document.querySelector('header').innerHTML = h; document.querySelector('footer').innerHTML = f; initHeaderInteractions(); initFooterInteractions(); } function initHeaderInteractions() { const themeBtn = document.querySelector('[data-theme-toggle]'); const themeModal = document.getElementById('themeModal'); const loginBtn = document.querySelector('[data-login]'); const regBtn = document.querySelector('[data-register]'); const loginModal = document.getElementById('loginModal'); const registerModal = document.getElementById('registerModal'); const applyTheme = (mode) => { localStorage.setItem('theme', mode); if (mode === 'dark') { document.documentElement.classList.add('dark'); document.body.classList.add('bg-slate-900','text-white'); document.body.classList.remove('bg-white','text-gray-900'); } else { document.documentElement.classList.remove('dark'); document.body.classList.remove('bg-slate-900','text-white'); document.body.classList.add('bg-white','text-gray-900'); } }; const stored = localStorage.getItem('theme') || 'light'; applyTheme(stored); themeBtn && themeBtn.addEventListener('click', () => themeModal && themeModal.showModal()); document.querySelectorAll('[data-select-theme]').forEach(el => el.addEventListener('click', () => { applyTheme(el.getAttribute('data-select-theme')); themeModal && themeModal.close(); })); loginBtn && loginBtn.addEventListener('click', () => loginModal && loginModal.showModal()); regBtn && regBtn.addEventListener('click', () => registerModal && registerModal.showModal()); document.querySelectorAll('[data-close-modal]').forEach(btn => btn.addEventListener('click', (e)=>e.target.closest('dialog') && e.target.closest('dialog').close())); } function initFooterInteractions() { const banner = document.getElementById('cookieBanner'); if (!localStorage.getItem('cookieConsent')) banner && banner.classList.remove('hidden'); const ca = document.getElementById('cookieAccept'); ca && ca.addEventListener('click', ()=>{ localStorage.setItem('cookieConsent','1'); banner && banner.classList.add('hidden'); }); const toTop = document.getElementById('toTop'); toTop && toTop.addEventListener('click', ()=>window.scrollTo({top:0, behavior:'smooth'})); } function clampPage(total) { const pages = Math.max(1, Math.ceil(total / pageSize)); if (currentPage > pages) currentPage = pages; if (currentPage < 1) currentPage = 1; return pages; } function renderList(items, page=1) { filtered = items; clampPage(items.length); const start = (currentPage-1)*pageSize; const slice = items.slice(start, start+pageSize); const ul = document.getElementById('items'); ul.innerHTML = ''; const favSet = new Set(JSON.parse(localStorage.getItem('favorites')||'[]')); slice.forEach(it => { const li = document.createElement('li'); li.className = 'border border-gray-200 rounded p-4 flex flex-col gap-3'; const rating = renderStars(it.rating || 0); li.innerHTML = `

      ${it.title}

      ${it.category} • ${it.level} • ${it.durationMin} min

      ${rating}${(it.rating||0).toFixed(1)} (${it.reviewsCount||0})

      ${escapeHTML(it.description.substring(0,160))}…

      € ${Number(it.priceEUR||0).toFixed(2)}
      `; ul.appendChild(li); }); const noRes = document.getElementById('noResults'); if (items.length === 0) { noRes.classList.remove('hidden'); } else { noRes.classList.add('hidden'); } renderPagination(items.length); } function renderStars(r) { const full = Math.floor(r); const half = r - full >= 0.5 ? 1 : 0; const empty = 5 - full - half; let s = ''; for (let i=0;i{ if (currentPage>1){ currentPage--; applyFilters(false); }}); p.appendChild(prev); for (let i=1;i<=pages;i++) { const b = document.createElement('button'); b.className = 'px-3 py-1 rounded border ' + (i===currentPage ? 'bg-blue-600 text-white border-blue-600' : 'border-gray-300 hover:bg-gray-50'); b.textContent = i; b.addEventListener('click', ()=>{ currentPage=i; applyFilters(false); }); p.appendChild(b); } const next = document.createElement('button'); next.className = 'px-3 py-1 rounded border border-gray-300 hover:bg-gray-50'; next.textContent = 'Volgende'; next.disabled = currentPage===pages; next.addEventListener('click', ()=>{ if (currentPage"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } function applyFilters(resetPage=true) { const fd = new FormData(document.getElementById('filterForm')); const q = (fd.get('q')||'').toString().toLowerCase().trim(); const category = fd.get('category')||''; const level = fd.get('level')||''; const location = fd.get('location')||''; const priceMinStr = fd.get('priceMin'); const priceMaxStr = fd.get('priceMax'); const priceMin = priceMinStr ? parseFloat(priceMinStr.toString()) : null; const priceMax = priceMaxStr ? parseFloat(priceMaxStr.toString()) : null; let items = data.filter(it => { const matchQ = q ? (it.title.toLowerCase().includes(q) || it.description.toLowerCase().includes(q) || (it.tags||[]).join(' ').toLowerCase().includes(q) || it.slug.toLowerCase().includes(q)) : true; const matchC = category ? it.category===category : true; const matchL = level ? it.level===level : true; const matchLoc = location ? (it.locations||[]).includes(location) : true; const matchPrice = (priceMin!==null ? it.priceEUR>=priceMin : true) && (priceMax!==null ? it.priceEUR<=priceMax : true); const matchDur = (extraFilters.durationMax!==null ? it.durationMin<=extraFilters.durationMax : true); const matchRating = (extraFilters.minRating!==null ? (it.rating||0)>=extraFilters.minRating : true); return matchQ && matchC && matchL && matchLoc && matchPrice && matchDur && matchRating; }); if (resetPage) currentPage = 1; updateActiveChipsUI({ q, category, level, location, priceMin, priceMax }); renderList(items, currentPage); } function updateActiveChipsUI(filters) { const wrap = document.getElementById('activeChips'); const chips = []; if (filters.q) chips.push({k:'q',label:`Zoek: ${filters.q}`}); if (filters.category) chips.push({k:'category',label:`Categorie: ${filters.category}`}); if (filters.level) chips.push({k:'level',label:`Niveau: ${filters.level}`}); if (filters.location) chips.push({k:'location',label:`Locatie: ${filters.location}`}); if (filters.priceMin!==null) chips.push({k:'priceMin',label:`Min: €${filters.priceMin}`}); if (filters.priceMax!==null) chips.push({k:'priceMax',label:`Max: €${filters.priceMax}`}); if (extraFilters.durationMax!==null) chips.push({k:'durationMax',label:`≤ ${extraFilters.durationMax} min`}); if (extraFilters.minRating!==null) chips.push({k:'minRating',label:`≥ ${extraFilters.minRating}★`}); wrap.innerHTML = ''; if (chips.length===0 && activePresetBtns.size===0) { wrap.classList.add('hidden'); return; } wrap.classList.remove('hidden'); chips.forEach(ch => { const b = document.createElement('button'); b.type = 'button'; b.setAttribute('data-clear', ch.k); b.className = 'px-3 py-1 rounded-full border border-blue-200 bg-blue-50 text-blue-800 text-sm'; b.innerHTML = `${ch.label} ×`; wrap.appendChild(b); }); if (activePresetBtns.size>0) { activePresetBtns.forEach(name => { const b = document.createElement('button'); b.type = 'button'; b.setAttribute('data-clear-preset', name); b.className = 'px-3 py-1 rounded-full border border-emerald-200 bg-emerald-50 text-emerald-800 text-sm'; b.innerHTML = `Preset: ${name} ×`; wrap.appendChild(b); }); } } function openDetails(id) { const it = data.find(x=>x.id===id); if (!it) return; document.getElementById('mTitle').textContent = it.title; document.getElementById('mDesc').textContent = it.description; document.getElementById('mCat').textContent = it.category; document.getElementById('mLvl').textContent = it.level; document.getElementById('mDur').textContent = it.durationMin; document.getElementById('mPrice').textContent = Number(it.priceEUR||0).toFixed(2); const ul = document.getElementById('mSched'); ul.innerHTML=''; (it.schedule||[]).forEach(s => { const li = document.createElement('li'); li.textContent = s; ul.appendChild(li); }); document.getElementById('mFav').onclick = ()=>toggleFavorite(id, true); document.getElementById('mCart').onclick = ()=>addToCart(id, true); document.getElementById('itemModal').showModal(); } function toggleFavorite(id, feedback=false) { const favs = new Set(JSON.parse(localStorage.getItem('favorites')||'[]')); if (favs.has(id)) favs.delete(id); else favs.add(id); localStorage.setItem('favorites', JSON.stringify([...favs])); applyFilters(false); if (feedback) { const dlg = document.createElement('dialog'); dlg.className = 'rounded-lg p-0 w-full max-w-sm'; dlg.innerHTML = '

      Favorieten bijgewerkt.

      '; document.body.appendChild(dlg); dlg.showModal(); dlg.querySelector('[data-close-modal]').addEventListener('click', ()=>{ dlg.close(); dlg.remove(); }); } } function readCart() { try { return JSON.parse(localStorage.getItem('cart')||'[]'); } catch(e){ return []; } } function writeCart(cart) { localStorage.setItem('cart', JSON.stringify(cart)); updateCartUI(); } function addToCart(id, withChoice=false) { const cart = readCart(); const idx = cart.findIndex(x=>x.id===id); if (idx>-1) cart[idx].qty += 1; else cart.push({id, qty:1}); writeCart(cart); if (withChoice) { const dlg = document.createElement('dialog'); dlg.className = 'rounded-lg p-0 w-full max-w-sm'; dlg.innerHTML = '

      Toegevoegd aan mandje.

      '; document.body.appendChild(dlg); dlg.showModal(); dlg.querySelector('[data-close-modal]').addEventListener('click', ()=>{ dlg.close(); dlg.remove(); }); dlg.querySelector('[data-open-cart]').addEventListener('click', ()=>{ dlg.close(); dlg.remove(); openCart(); }); } } function openCart() { updateCartUI(); document.getElementById('cartModal').showModal(); } function updateCartUI() { const cart = readCart(); const cnt = cart.reduce((a,c)=>a+(c.qty||0),0); const countEl = document.getElementById('cartCount'); if (countEl) countEl.textContent = String(cnt); const list = document.getElementById('cartList'); const itemsCountEl = document.getElementById('cartItemsCount'); const totalEl = document.getElementById('cartTotal'); if (!list) return; list.innerHTML = ''; let total = 0; cart.forEach(row=>{ const it = data.find(x=>x.id===row.id); const title = it ? it.title : row.id; const price = it ? Number(it.priceEUR||0) : 0; const lineTotal = price * (row.qty||1); total += lineTotal; const li = document.createElement('li'); li.className = 'py-3 flex items-center justify-between gap-4'; li.innerHTML = `
      ${escapeHTML(title)}
      € ${price.toFixed(2)} • qty: ${row.qty||1}
      € ${lineTotal.toFixed(2)}
      `; list.appendChild(li); }); if (itemsCountEl) itemsCountEl.textContent = String(cnt); if (totalEl) totalEl.textContent = total.toFixed(2); } document.addEventListener('click', (e)=>{ const fav = e.target.closest('[data-fav]'); if (fav) { toggleFavorite(fav.getAttribute('data-fav')); return; } const det = e.target.closest('[data-details]'); if (det) { openDetails(det.getAttribute('data-details')); return; } const cartAdd = e.target.closest('[data-addcart]'); if (cartAdd) { addToCart(cartAdd.getAttribute('data-addcart'), true); return; } const close = e.target.closest('[data-close-modal]'); if (close) { const d = close.closest('dialog'); if (d) d.close(); return; } const fab = e.target.closest('#cartFab'); if (fab) { openCart(); return; } const dec = e.target.closest('[data-qty-dec]'); if (dec) { const id = dec.getAttribute('data-qty-dec'); const cart = readCart(); const idx = cart.findIndex(x=>x.id===id); if (idx>-1) { cart[idx].qty = Math.max(1,(cart[idx].qty||1)-1); writeCart(cart); } return; } const inc = e.target.closest('[data-qty-inc]'); if (inc) { const id = inc.getAttribute('data-qty-inc'); const cart = readCart(); const idx = cart.findIndex(x=>x.id===id); if (idx>-1) { cart[idx].qty = (cart[idx].qty||1)+1; writeCart(cart); } return; } const rem = e.target.closest('[data-remove]'); if (rem) { const id = rem.getAttribute('data-remove'); let cart = readCart(); cart = cart.filter(x=>x.id!==id); writeCart(cart); return; } const clearPreset = e.target.closest('[data-clear-preset]'); if (clearPreset) { const name = clearPreset.getAttribute('data-clear-preset'); presetDeactivateByName(name); applyFilters(); return; } const clearChip = e.target.closest('[data-clear]'); if (clearChip) { const k = clearChip.getAttribute('data-clear'); const form = document.getElementById('filterForm'); if (k==='q') form.elements['q'].value=''; if (k==='category') form.elements['category'].value=''; if (k==='level') form.elements['level'].value=''; if (k==='location') form.elements['location'].value=''; if (k==='priceMin') form.elements['priceMin'].value=''; if (k==='priceMax') form.elements['priceMax'].value=''; if (k==='durationMax') extraFilters.durationMax = null; if (k==='minRating') extraFilters.minRating = null; currentPage=1; applyFilters(); return; } }); document.getElementById('cartClear').addEventListener('click', ()=>{ writeCart([]); }); document.getElementById('cartClose').addEventListener('click', ()=>document.getElementById('cartModal').close()); const formEl = document.getElementById('filterForm'); formEl.addEventListener('input', ()=>{ currentPage=1; applyFilters(false); }); formEl.addEventListener('reset', (e)=>{ setTimeout(()=>{ // allow native reset to update inputs extraFilters = { durationMax: null, minRating: null }; presetDeactivateAll(); currentPage=1; applyFilters(); },0); }); document.querySelectorAll('[data-preset]').forEach(b => b.addEventListener('click', ()=>{ const preset = JSON.parse(b.getAttribute('data-preset')); const form = document.getElementById('filterForm'); if (typeof preset.q!=='undefined') form.elements['q'].value = preset.q; if (typeof preset.category!=='undefined') form.elements['category'].value = preset.category; if (typeof preset.level!=='undefined') form.elements['level'].value = preset.level; if (typeof preset.location!=='undefined') form.elements['location'].value = preset.location; if (typeof preset.priceMin!=='undefined') form.elements['priceMin'].value = preset.priceMin; if (typeof preset.priceMax!=='undefined') form.elements['priceMax'].value = preset.priceMax; activatePresetButton(b); currentPage=1; applyFilters(); })); document.querySelectorAll('[data-extra]').forEach(b => b.addEventListener('click', ()=>{ const ext = JSON.parse(b.getAttribute('data-extra')); if (typeof ext.durationMax!=='undefined') extraFilters.durationMax = ext.durationMax; if (typeof ext.minRating!=='undefined') extraFilters.minRating = ext.minRating; activatePresetButton(b); currentPage=1; applyFilters(); })); function activatePresetButton(btn) { const label = btn.textContent.trim(); activePresetBtns.add(label); btn.classList.add('border-blue-300','bg-blue-50','text-blue-800'); } function presetDeactivateByName(name) { activePresetBtns.delete(name); document.querySelectorAll('button').forEach(b=>{ if (b.textContent.trim()===name) { b.classList.remove('border-blue-300','bg-blue-50','text-blue-800'); if (b.hasAttribute('data-extra')) { const ext = JSON.parse(b.getAttribute('data-extra')); if (typeof ext.durationMax!=='undefined') extraFilters.durationMax=null; if (typeof ext.minRating!=='undefined') extraFilters.minRating=null; } if (b.hasAttribute('data-preset')) { const preset = JSON.parse(b.getAttribute('data-preset')); const form = document.getElementById('filterForm'); if (typeof preset.q!=='undefined') form.elements['q'].value=''; if (typeof preset.category!=='undefined') form.elements['category'].value=''; if (typeof preset.level!=='undefined') form.elements['level'].value=''; if (typeof preset.location!=='undefined') form.elements['location'].value=''; if (typeof preset.priceMin!=='undefined') form.elements['priceMin'].value=''; if (typeof preset.priceMax!=='undefined') form.elements['priceMax'].value=''; } } }); } function presetDeactivateAll() { activePresetBtns.clear(); document.querySelectorAll('.t3m0a,.g8lmn,.z9qpl,.y7snc,.a1vke,.r2cww').forEach(b=>{ b.classList.remove('border-blue-300','bg-blue-50','text-blue-800'); }); } // Keyboard shortcut to focus search window.addEventListener('keydown', (e)=>{ const tag = (document.activeElement && document.activeElement.tagName) || ''; if (e.key === '/' && tag !== 'INPUT' && tag !== 'TEXTAREA') { e.preventDefault(); document.getElementById('searchInput').focus(); } }); document.getElementById('nrReset').addEventListener('click', ()=>{ formEl.reset(); extraFilters = { durationMax: null, minRating: null }; presetDeactivateAll(); currentPage=1; applyFilters(); }); document.getElementById('nrStarter').addEventListener('click', ()=>{ const btn = document.querySelector('.t3m0a'); btn && btn.click(); }); (async function(){ await injectLayout(); try { data = await fetch('catalog.json', {cache:'no-store'}).then(r=>r.json()); } catch (e) { data = []; } updateCartUI(); applyFilters(); })();