SFC
0 results
Favorites Cart
'), fetch('footer.html').then(r=>r.text()).catch(()=>' ') ]); document.querySelector('header').innerHTML = h; document.querySelector('footer').innerHTML = f; initLayoutInteractions(); } function initLayoutInteractions() { const prefer = localStorage.getItem(THEME_KEY) || 'light'; document.documentElement.classList.toggle('dark', prefer==='dark'); document.querySelectorAll('[data-nav-toggle]').forEach(b=>b.addEventListener('click', ()=>{ document.querySelector('[data-nav-mobile]')?.classList.toggle('hidden'); })); document.body.addEventListener('click', (e)=>{ if (e.target.matches('[data-close-modal]')) e.target.closest('.fixed.inset-0')?.classList.add('hidden'); }); document.querySelector('[data-quick-theme]')?.addEventListener('click', ()=>{ const isDark = document.documentElement.classList.toggle('dark'); localStorage.setItem(THEME_KEY, isDark ? 'dark' : 'light'); }); const banner = document.querySelector('[data-cookie-banner]'); if (!localStorage.getItem('cookieConsent')) banner?.classList.remove('hidden'); document.querySelector('[data-cookie-accept]')?.addEventListener('click', ()=>{ localStorage.setItem('cookieConsent', JSON.stringify({necessary:true, analytics:true, ts:Date.now()})); banner?.classList.add('hidden'); }); document.querySelector('[data-cookie-settings]')?.addEventListener('click', ()=>document.getElementById('cookie-modal')?.classList.remove('hidden')); document.querySelector('[data-cookie-save]')?.addEventListener('click', ()=>{ const analytics=document.getElementById('cookie-analytics')?.checked??false; localStorage.setItem('cookieConsent', JSON.stringify({necessary:true, analytics, ts:Date.now()})); document.getElementById('cookie-modal')?.classList.add('hidden'); banner?.classList.add('hidden'); }); } function readFilters() { return { q: document.getElementById('q').value.trim().toLowerCase(), cat: document.getElementById('f-category').value, lvl: document.getElementById('f-level').value, min: Number(document.getElementById('f-min').value || 0), max: Number(document.getElementById('f-max').value || Infinity) }; } function filterData() { const {q,cat,lvl,min,max} = readFilters(); return DATA.filter(x=>{ const txt = (x.title+' '+x.short+' '+x.description+' '+x.tags.join(' ')).toLowerCase(); const okQ = q? txt.includes(q) : true; const okC = cat? x.category===cat : true; const okL = lvl? x.level===lvl : true; const okP = x.priceUSD>=min && x.priceUSD<=max; return okQ && okC && okL && okP; }); } function render() { const list = filterData(); const total = list.length; const pages = Math.max(1, Math.ceil(total / SIZE)); if (PAGE>pages) PAGE = pages; const start = (PAGE-1)*SIZE; const slice = list.slice(start, start+SIZE); document.getElementById('result-meta').textContent = `${total} result${total===1?'':'s'} • page ${PAGE}/${pages}`; const favs = new Set(JSON.parse(localStorage.getItem(FAVORITES_KEY)||'[]')); const grid = document.getElementById('grid'); grid.innerHTML = slice.map(c=>`

${c.title}${c.bestseller?'Bestseller':''}

${c.short}

${c.level} ${c.category} ${c.durationHours}h • ${c.lessons} lessons ★ ${c.rating.toFixed(1)}
$${c.priceUSD.toFixed(2)}
`).join('') || '

No results. Try adjusting filters.

'; const pager = document.getElementById('pager'); pager.innerHTML = ''; const mk = (label, page, disabled=false, current=false)=>``; pager.innerHTML += mk('Prev', Math.max(1, PAGE-1), PAGE===1); for (let i=1;i<=pages;i++){ if (i===1 || i===pages || Math.abs(i-PAGE)<=1) pager.innerHTML += mk(i, i, false, i===PAGE); else if (i===2 && PAGE>3) pager.innerHTML += ``; else if (i===pages-1 && PAGE…`; } pager.innerHTML += mk('Next', Math.min(pages, PAGE+1), PAGE===pages); document.querySelectorAll('[data-page]').forEach(b=>b.addEventListener('click',()=>{ PAGE=Number(b.getAttribute('data-page')); render(); window.scrollTo({top:0, behavior:'smooth'}); })); document.querySelectorAll('[data-fav]').forEach(b=>b.addEventListener('click',()=>{ const id=Number(b.getAttribute('data-fav')); const s=new Set(JSON.parse(localStorage.getItem(FAVORITES_KEY)||'[]')); if (s.has(id)) s.delete(id); else s.add(id); localStorage.setItem(FAVORITES_KEY, JSON.stringify([...s])); b.textContent=s.has(id)?'Saved':'Save'; })); document.querySelectorAll('[data-add]').forEach(b=>b.addEventListener('click',()=>{ const id=Number(b.getAttribute('data-add')); const cart=JSON.parse(localStorage.getItem(CART_KEY)||'[]'); const i=cart.findIndex(x=>x.id===id); if (i>-1) cart[i].qty+=1; else cart.push({id, qty:1}); localStorage.setItem(CART_KEY, JSON.stringify(cart)); b.textContent='Added'; setTimeout(()=>b.textContent='Add', 1200); })); document.querySelectorAll('[data-detail]').forEach(b=>b.addEventListener('click',()=>{ const id=Number(b.getAttribute('data-detail')); const c=DATA.find(x=>x.id===id); LAST_DETAIL=c; document.getElementById('d-title').textContent=c.title; document.getElementById('d-badges').innerHTML = ` ${c.level} ${c.category} ${c.durationHours}h • ${c.lessons} lessons ★ ${c.rating.toFixed(1)} `; document.getElementById('d-desc').textContent=c.description; document.getElementById('d-meta').textContent = `Instructors: ${c.instructors.join(', ')} • Updated: ${new Date(c.updatedAt).toLocaleDateString()} • Language: ${c.language}`; document.getElementById('d-fav').textContent = (new Set(JSON.parse(localStorage.getItem(FAVORITES_KEY)||'[]'))).has(c.id)?'Saved':'Save'; document.getElementById('details-modal').classList.remove('hidden'); })); } async function boot() { await injectShared(); try { const res = await fetch('catalog.json', {cache:'no-store'}); if (!res.ok) throw new Error('Failed to load catalog.json'); DATA = await res.json(); } catch (e) { document.getElementById('grid').innerHTML = '

Unable to load catalog. Please try again later.

'; return; } const hash = location.hash.replace('#',''); if (hash) setTimeout(()=>document.getElementById(hash)?.scrollIntoView({behavior:'smooth'}), 300); ['q','f-category','f-level','f-min','f-max'].forEach(id=>{ document.getElementById(id).addEventListener('input', ()=>{ PAGE=1; render(); }); }); document.getElementById('f-clear').addEventListener('click', ()=>{ document.getElementById('q').value=''; document.getElementById('f-category').value=''; document.getElementById('f-level').value=''; document.getElementById('f-min').value=''; document.getElementById('f-max').value=''; PAGE=1; render(); }); document.getElementById('help-shortcuts').addEventListener('click', ()=>document.getElementById('help-modal').classList.remove('hidden')); document.addEventListener('keydown', (e)=>{ const ae=document.activeElement; const typing = ae && (ae.tagName==='INPUT' || ae.tagName==='TEXTAREA' || ae.isContentEditable); if (e.key==='/'){ e.preventDefault(); document.getElementById('q').focus(); } if (!typing) { if (e.key==='j'){ e.preventDefault(); PAGE+=1; render(); } if (e.key==='k'){ e.preventDefault(); PAGE=Math.max(1, PAGE-1); render(); } if (e.key==='Escape'){ document.querySelectorAll('.fixed.inset-0').forEach(m=>{ if (!m.classList.contains('hidden')) m.classList.add('hidden'); }); } } }); document.getElementById('filters-form').addEventListener('submit', (e)=>{ e.preventDefault(); PAGE=1; render(); }); document.getElementById('d-fav').addEventListener('click', ()=>{ const s=new Set(JSON.parse(localStorage.getItem(FAVORITES_KEY)||'[]')); if (!LAST_DETAIL) return; if (s.has(LAST_DETAIL.id)) s.delete(LAST_DETAIL.id); else s.add(LAST_DETAIL.id); localStorage.setItem(FAVORITES_KEY, JSON.stringify([...s])); document.getElementById('d-fav').textContent = s.has(LAST_DETAIL.id)?'Saved':'Save'; render(); }); document.getElementById('d-cart').addEventListener('click', ()=>{ if (!LAST_DETAIL) return; const cart=JSON.parse(localStorage.getItem(CART_KEY)||'[]'); const i=cart.findIndex(x=>x.id===LAST_DETAIL.id); if (i>-1) cart[i].qty+=1; else cart.push({id:LAST_DETAIL.id, qty:1}); localStorage.setItem(CART_KEY, JSON.stringify(cart)); document.getElementById('d-cart').textContent='Added'; setTimeout(()=>document.getElementById('d-cart').textContent='Add to cart', 1200); }); render(); } boot();