Tracked Courses
Manage favorites, move items to cart, and export your tracked list as JSON.
No tracked courses yet. Explore the catalog.
'
: '';
});
}
function initCommonDialogs(){
document.querySelectorAll('dialog').forEach(d=>{
d.addEventListener('click', e=>{
if(e.target===d) d.close();
});
});
document.addEventListener('keydown', e=>{
if(e.key==='Escape'){
const open=[...document.querySelectorAll('dialog')].filter(d=>d.open);
if(open.length) open[open.length-1].close();
}
});
}
const state = {
DATA: [],
subset: [],
query: '',
sort: 'recent'
};
const searchEl=document.getElementById('search');
const sortEl=document.getElementById('sort');
const exportBtn=document.getElementById('export');
const clearBtn=document.getElementById('clear-all');
function normalizeId(v){
if(v===null || v===undefined) return '';
return String(v);
}
function ensureMetaForFavIds(ids){
const meta=getFavMeta();
let changed=false;
const now=Date.now();
ids.forEach(id=>{
if(!meta[id]){
meta[id]={trackedAt: now};
changed=true;
}
});
if(changed) setFavMeta(meta);
return meta;
}
function buildSubset(){
const favIdsRaw=getFav().map(normalizeId).filter(Boolean);
const favSet=new Set(favIdsRaw);
const meta=ensureMetaForFavIds(favSet);
let items=state.DATA.filter(x=>favSet.has(normalizeId(x.id)));
const q=(state.query||'').trim().toLowerCase();
if(q){
items = items.filter(it=>{
const t = (it.title||'').toLowerCase();
const d = (it.shortDescription||'').toLowerCase();
const c = (it.category||'').toLowerCase();
const l = (it.level||'').toLowerCase();
return t.includes(q) || d.includes(q) || c.includes(q) || l.includes(q);
});
}
const sort=state.sort || 'recent';
items.sort((a,b)=>{
const aid=normalizeId(a.id), bid=normalizeId(b.id);
const am=meta[aid]?.trackedAt || 0;
const bm=meta[bid]?.trackedAt || 0;
const at=(a.title||'').toLowerCase();
const bt=(b.title||'').toLowerCase();
const ap=Number(a.price||0);
const bp=Number(b.price||0);
const ar=Number(a.rating||0);
const br=Number(b.rating||0);
if(sort==='recent') return bm-am;
if(sort==='title') return at.localeCompare(bt);
if(sort==='price_asc') return ap-bp;
if(sort==='price_desc') return bp-ap;
if(sort==='rating_desc') return br-ar;
return bm-am;
});
state.subset=items;
}
function money(v){
const n=Number(v);
if(Number.isFinite(n)) return '$'+n.toFixed(2);
return '$0.00';
}
function clampText(s, max){
const str=String(s||'');
if(str.length<=max) return str;
return str.slice(0, max-1)+'…';
}
function render(){
buildSubset();
const L=document.getElementById('list');
const empty=document.getElementById('empty');
const count=document.getElementById('count');
L.innerHTML='';
const favTotal=new Set(getFav().map(normalizeId).filter(Boolean)).size;
count.textContent=String(favTotal);
empty.classList.toggle('hidden', state.subset.length>0 || favTotal>0 ? state.subset.length>0 : false);
if(favTotal===0){
empty.classList.remove('hidden');
return;
}
if(state.subset.length===0){
empty.innerHTML='No matches. Clear your search or explore the catalog.';
empty.classList.remove('hidden');
return;
} else {
empty.innerHTML='No tracked courses yet. Explore the catalog.';
empty.classList.add('hidden');
}
const cart=getCart();
state.subset.forEach(it=>{
const id=normalizeId(it.id);
const qty=Number(cart[id]||0);
const li=document.createElement('li');
li.className='border border-gray-200 rounded-xl p-4 sm:p-5 bg-white shadow-sm hover:shadow-md transition-shadow flex flex-col';
li.innerHTML=`
${escapeHtml(it.title||'Untitled course')}
Tracked
${escapeHtml(clampText(it.shortDescription||'', 180))}
Category: ${escapeHtml(it.category||'—')}
Level: ${escapeHtml(it.level||'—')}
Duration: ${escapeHtml(String(it.durationHours ?? '—'))}h
Rating: ⭐ ${escapeHtml(String(it.rating ?? '—'))}
${escapeHtml(money(it.price))}
In cart: ${qty}
`;
L.appendChild(li);
});
L.querySelectorAll('[data-untrack]').forEach(b=>b.addEventListener('click',()=>{
const id=normalizeId(b.getAttribute('data-untrack'));
const favs=getFav().map(normalizeId).filter(Boolean);
const next=favs.filter(x=>x!==id);
setFav(next);
const meta=getFavMeta();
if(meta[id]){ delete meta[id]; setFavMeta(meta); }
render();
showToast('Removed', 'Course removed from tracked list.');
}));
L.querySelectorAll('[data-add]').forEach(b=>b.addEventListener('click',()=>{
const id=normalizeId(b.getAttribute('data-add'));
const cart=getCart();
cart[id]=Number(cart[id]||0)+1;
setCart(cart);
render();
showToast('Added to cart', 'The course was added to your cart.');
}));
}
function escapeHtml(str){
return String(str)
.replaceAll('&','&')
.replaceAll('<','<')
.replaceAll('>','>')
.replaceAll('"','"')
.replaceAll("'","'");
}
function showToast(title, text){
const dlg=document.getElementById('toast-modal');
const t=document.getElementById('toast-title');
const p=document.getElementById('toast-text');
t.textContent=title;
p.textContent=text;
try { dlg.showModal(); } catch { dlg.setAttribute('open',''); }
}
function exportSubset(){
const favs=new Set(getFav().map(normalizeId).filter(Boolean));
const subset=state.DATA.filter(x=>favs.has(normalizeId(x.id)));
const dlg=document.getElementById('export-modal');
const area=document.getElementById('export-text');
const copyStatus=document.getElementById('copy-status');
copyStatus.classList.add('hidden');
area.value=JSON.stringify(subset, null, 2);
try { dlg.showModal(); } catch { dlg.setAttribute('open',''); }
setTimeout(()=>area.focus(), 50);
setTimeout(()=>{ area.setSelectionRange(0, area.value.length); }, 80);
}
document.getElementById('copy-export').addEventListener('click', async ()=>{
const area=document.getElementById('export-text');
const copyStatus=document.getElementById('copy-status');
const txt=area.value||'';
let ok=false;
try{
await navigator.clipboard.writeText(txt);
ok=true;
}catch(e){
try{
area.focus();
area.select();
ok=document.execCommand('copy');
}catch(e2){
ok=false;
}
}
copyStatus.textContent = ok ? 'Copied to clipboard.' : 'Copy failed. Select text and copy manually.';
copyStatus.classList.remove('hidden');
copyStatus.classList.toggle('text-emerald-700', ok);
copyStatus.classList.toggle('text-gray-700', !ok);
});
exportBtn.addEventListener('click', exportSubset);
clearBtn.addEventListener('click', ()=>{
const favs=getFav().map(normalizeId).filter(Boolean);
if(favs.length===0){
showToast('Nothing to clear', 'Your tracked list is already empty.');
return;
}
const dlg=document.getElementById('confirm-clear-modal');
try { dlg.showModal(); } catch { dlg.setAttribute('open',''); }
});
document.getElementById('confirm-clear').addEventListener('click', ()=>{
setFav([]);
setFavMeta({});
const dlg=document.getElementById('confirm-clear-modal');
dlg.close();
render();
showToast('Cleared', 'All tracked courses were removed.');
});
searchEl.addEventListener('input', ()=>{
state.query=searchEl.value||'';
render();
});
sortEl.addEventListener('change', ()=>{
state.sort=sortEl.value||'recent';
render();
});
fetch('catalog.json', {cache:'no-store'})
.then(r=>{
if(!r.ok) throw new Error('Failed to load catalog.json');
return r.json();
})
.then(DATA=>{
if(!Array.isArray(DATA)) DATA=[];
state.DATA=DATA;
const favIds=getFav().map(normalizeId).filter(Boolean);
ensureMetaForFavIds(new Set(favIds));
render();
})
.catch(()=>{
state.DATA=[];
render();
showToast('Catalog unavailable', 'Could not load catalog.json. Please try again later.');
});
})();