Login

Create account

Theme

Favorites

Export / Import

"font-semibold text-neutral-900 dark:text-neutral-100">${it.title}
${courseSummary(it)}
`; ul.appendChild(li); }); } function move(id, dir){ const idx = fav.indexOf(String(id)); if (idx<0) return; const ni = idx + (dir==='up'?-1:1); if (ni<0 || ni>=fav.length) return; const [item] = fav.splice(idx,1); fav.splice(ni,0,item); store.set('favorites', fav); render(); try{ const it = all.find(x=>String(x.id)===String(id)); if (it) announce(`${it.title} moved ${dir}`); }catch{} } function removeItem(id){ const it = all.find(x=>String(x.id)===String(id)); fav = fav.filter(x=>String(x)!==String(id)); localStorage.setItem('favorites', JSON.stringify(fav)); render(); if (it) announce(`${it.title} removed from tracked`); } function indexFromEvent(e, li){ const rect = li.getBoundingClientRect(); const mid = rect.top + rect.height/2; return e.clientY < mid ? 'before' : 'after'; } function clearDropHints(){ [...listEl.children].forEach(ch=>{ ch.classList.remove('f6y0m-drop-before','f6y0m-drop-after'); }); } function enableDnD(){ listEl.addEventListener('dragstart', (e)=>{ const li = e.target.closest('li[data-id]'); if (!li) return; draggingId = li.getAttribute('data-id'); li.classList.add('p2aql-dragging'); e.dataTransfer.effectAllowed='move'; try{ const crt = li.cloneNode(true); crt.style.position='absolute'; crt.style.top='-9999px'; document.body.appendChild(crt); e.dataTransfer.setDragImage(crt, 20, 20); setTimeout(()=>document.body.removeChild(crt),0); }catch{} }); listEl.addEventListener('dragend', (e)=>{ const li = e.target.closest('li[data-id]'); if (li) li.classList.remove('p2aql-dragging'); draggingId = null; clearDropHints(); }); listEl.addEventListener('dragover', (e)=>{ e.preventDefault(); const overLi = e.target.closest('li[data-id]'); clearDropHints(); if (!overLi) return; const where = indexFromEvent(e, overLi); overLi.classList.add(where==='before'?'f6y0m-drop-before':'f6y0m-drop-after'); }); listEl.addEventListener('drop', (e)=>{ e.preventDefault(); const overLi = e.target.closest('li[data-id]'); const srcId = draggingId; if (!srcId) { clearDropHints(); return; } if (!overLi){ // drop to end const srcIdx = fav.indexOf(String(srcId)); const [it] = fav.splice(srcIdx,1); fav.push(it); store.set('favorites', fav); render(); const ent = all.find(x=>String(x.id)===String(srcId)); if (ent) announce(`${ent.title} moved to end`); return; } const destId = overLi.getAttribute('data-id'); if (destId===srcId){ clearDropHints(); return; } const where = indexFromEvent(e, overLi); const srcIdx = fav.indexOf(String(srcId)); const destIdx = fav.indexOf(String(destId)); if (srcIdx<0 || destIdx<0){ clearDropHints(); return; } const [it] = fav.splice(srcIdx,1); let insertAt = destIdx + (where==='after'?1:0); if (srcIdx < destIdx && where==='before') insertAt -= 1; if (srcIdx > destIdx && where==='after') insertAt += 0; if (insertAt<0) insertAt = 0; if (insertAt>fav.length) insertAt = fav.length; fav.splice(insertAt,0,it); store.set('favorites', fav); render(); const ent = all.find(x=>String(x.id)===String(srcId)); if (ent) announce(`${ent.title} reordered`); clearDropHints(); }); // Keyboard reorder: Alt + ArrowUp/Down listEl.addEventListener('keydown', (e)=>{ const li = e.target.closest('li[data-id]'); if (!li) return; const id = li.getAttribute('data-id'); if (e.altKey && (e.key==='ArrowUp' || e.key==='ArrowDown')){ e.preventDefault(); move(id, e.key==='ArrowUp'?'up':'down'); const nextLi = [...listEl.children].find(el=>el.getAttribute('data-id')===id); nextLi?.focus(); } if (e.key==='Delete'){ e.preventDefault(); removeItem(id); } }); // Start drag by handle only listEl.addEventListener('mousedown', (e)=>{ const handle = e.target.closest('.xk9h7-handle'); const li = e.target.closest('li[data-id]'); if (!li) return; if (!handle){ li.draggable = false; }else{ li.draggable = true; } }); listEl.addEventListener('mouseup', (e)=>{ const li = e.target.closest('li[data-id]'); if (li) li.draggable = true; }); } async function init(){ await loadPartials(); normalizeFavorites(); try{ all = await fetch('catalog.json').then(r=>r.json()); }catch{ all = []; } render(); enableDnD(); themeInit(); listEl.addEventListener('click', (e)=>{ const up = e.target.closest('[data-up]'); const down = e.target.closest('[data-down]'); const rm = e.target.closest('[data-remove]'); if (up) move(up.getAttribute('data-up'), 'up'); if (down) move(down.getAttribute('data-down'), 'down'); if (rm) removeItem(rm.getAttribute('data-remove')); }); document.getElementById('clearAll').addEventListener('click', ()=>{ if (!fav.length) return; openModal('

Clear all tracked?

This action removes all tracked courses from this device. You can add them again from the Catalog anytime.

'); const m = document.querySelector('.fixed.inset-0'); m.querySelector('#caCancel').addEventListener('click', ()=>m.remove()); m.querySelector('#caConfirm').addEventListener('click', ()=>{ fav = []; store.set('favorites', fav); render(); announce('All tracked courses cleared'); m.remove(); }); }); document.getElementById('helpBtn').addEventListener('click', ()=>{ openModal('

Managing your tracked courses

'); }); window.addEventListener('storage', (e)=>{ if (e.key==='favorites'){ try{ const next = JSON.parse(e.newValue||'[]').map(String); fav = Array.isArray(next) ? next : []; render(); }catch{} } if (e.key==='theme'){ themeApply(store.get('theme','auto')); } }); } init();