Sales Funnel Courses

Tracked Courses

Your private space to manage favorites, notes and study sessions.

Filters

0 tracked 0 planned sessions 0 notes saved

Upcoming sessions

Today’s Momentum
Stay consistent. Your streak resets when the timer hits zero.

Streak resets in 24:00:00
Notes saved
Planned soon
Overdue
Tip: Export all notes as JSON for backup.
Sales Funnel Courses
'; }); fetch('./footer.html').then(r=>r.text()).then(html=>{ footerMount.innerHTML = html; }).catch(()=>{ footerMount.innerHTML=''; q('#yy').textContent = new Date().getFullYear(); }); const themeBtn = q('#themeToggle'); function applyTheme(){ try{ const t = localStorage.getItem('theme') || 'dark'; if(t==='dark'){ document.documentElement.classList.add('dark'); themeBtn.setAttribute('aria-pressed','true'); } else{ document.documentElement.classList.remove('dark'); themeBtn.setAttribute('aria-pressed','false'); } }catch(e){} } applyTheme(); themeBtn.addEventListener('click', ()=>{ const pressed = themeBtn.getAttribute('aria-pressed')==='true'; const next = pressed ? 'light':'dark'; try{ localStorage.setItem('theme', next); }catch(e){} applyTheme(); }); const state = { catalog: [], tracked: [], courses: [], removeTarget: null, filters: { search:'', tag:'', sort:'recent' }, consent: null }; function readTrackedIds(){ const keys = ['trackedCourses','favorites','favoriteCourses']; const acc = new Set(); keys.forEach(k=>{ try{ const raw = localStorage.getItem(k); if(!raw) return; if(raw.trim().startsWith('[')){ JSON.parse(raw).forEach(id=>acc.add(String(id))); }else{ raw.split(',').map(s=>s.trim()).filter(Boolean).forEach(id=>acc.add(String(id))); } }catch(e){} }); state.tracked = Array.from(acc); } function writeTrackedIds(){ try{ localStorage.setItem('trackedCourses', JSON.stringify(state.tracked)); }catch(e){} } function seedDemo(){ state.tracked = ['course-101','course-202','course-303']; writeTrackedIds(); render(); } function getVal(obj, keys, fallback=''){ for(const k of keys){ if(obj && typeof obj==='object' && obj[k]!=null) return obj[k]; } return fallback; } function priceToNumber(p){ if(typeof p==='number') return p; if(typeof p!=='string') return 0; const m = p.replace(/[^\d.]/g,''); return parseFloat(m||'0')||0; } function collectLevels(courses){ const set = new Set(); courses.forEach(c=>{ const lvl = (getVal(c,['level','difficulty'],'')||'').toString().toLowerCase(); if(lvl) set.add(lvl); }); return Array.from(set); } function buildStars(rating){ const r = Math.max(0, Math.min(5, Number(rating)||0)); let out=''; for(let i=1;i<=5;i++){ out += ``; } return out; } function noteKey(id){ return `note:${id}`; } function planKey(id){ return `plan:${id}`; } function getImageSrc(course){ const fromCatalog = getVal(course, ['image','img','thumbnail','cover'], ''); if(typeof fromCatalog==='string' && fromCatalog.startsWith('./images/')) return fromCatalog; return './images/maximum_ditailes_of_this_image.abstract_colorful_gradient_background_glassmorphism_blurred_bokeh_high_contrast_sharp_edges_ultra_hd_banner_courses_placeholder.jpg'; } function renderCounts(){ q('#countBadge').textContent = `${state.courses.length} tracked`; const notesCount = state.courses.reduce((acc,c)=>{ try{ return acc + (localStorage.getItem(noteKey(c.id))?1:0); }catch(e){return acc;} },0); q('#notesBadge').textContent = `${notesCount} notes saved`; const plansCount = state.courses.reduce((acc,c)=>{ try{ return acc + (localStorage.getItem(planKey(c.id))?1:0); }catch(e){return acc;} },0); q('#plannedBadge').textContent = `${plansCount} planned sessions`; } function humanDate(ts){ try{ const d = new Date(Number(ts)); if(isNaN(d.getTime())) return ''; return d.toLocaleString([], {hour12:false, year:'numeric', month:'short', day:'2-digit', hour:'2-digit', minute:'2-digit'}); }catch(e){return '';} } function render(){ readTrackedIds(); const wrap = q('#coursesWrap'); wrap.innerHTML = ''; if(!state.catalog.length){ wrap.innerHTML = ''; return; } const byId = new Map(state.catalog.map(c=>[String(c.id), c])); state.courses = state.tracked.map(id=>byId.get(String(id))).filter(Boolean); // Filters const s = state.filters.search.trim().toLowerCase(); const tag = state.filters.tag; let list = state.courses.filter(c=>{ const hay = [ getVal(c,['title','name','label'],''), getVal(c,['description','summary'],''), getVal(c,['provider','author'],'') ].join(' ').toLowerCase(); const passS = s? hay.includes(s):true; const lvl = (getVal(c,['level','difficulty'],'')||'').toString().toLowerCase(); const passT = tag? lvl===tag : true; return passS && passT; }); // Sort switch(state.filters.sort){ case 'title': list.sort((a,b)=>String(getVal(a,['title','name'],'')).localeCompare(String(getVal(b,['title','name'],'')).toString())); break; case 'priceAsc': list.sort((a,b)=>priceToNumber(getVal(a,['price','cost'],0)) - priceToNumber(getVal(b,['price','cost'],0))); break; case 'priceDesc': list.sort((a,b)=>priceToNumber(getVal(b,['price','cost'],0)) - priceToNumber(getVal(a,['price','cost'],0))); break; case 'rating': list.sort((a,b)=>(Number(getVal(b,['rating','score'],0))||0) - (Number(getVal(a,['rating','score'],0))||0)); break; default: list = list; // assume incoming order ~ recently added } if(!list.length){ q('#emptyState').style.display = 'block'; renderCounts(); return; }else{ q('#emptyState').style.display = 'none'; } // Levels hydrate filter if empty options (keep first “All levels”) const tf = q('#tagFilter'); if(tf && tf.options.length<=1){ const lvls = collectLevels(state.catalog); lvls.forEach(l=>{ const opt = document.createElement('option'); opt.value = l.toLowerCase(); opt.textContent = l[0].toUpperCase()+l.slice(1); tf.appendChild(opt); }); } const frag = document.createDocumentFragment(); list.forEach(course=>{ const id = String(course.id); const title = getVal(course,['title','name'],'Untitled course'); const provider = getVal(course,['provider','author'],'Independent'); const level = (getVal(course,['level','difficulty'],'')||'').toString(); const price = getVal(course,['price','cost'],'Free'); const rating = getVal(course,['rating','score'],0); const lessons = getVal(course,['lessons','modules','duration'],0); const tags = Array.isArray(course.tags)? course.tags.slice(0,4): []; let noteVal = ''; try{ noteVal = localStorage.getItem(noteKey(id)) || ''; }catch(e){} let planData=null; try{ planData = JSON.parse(localStorage.getItem(planKey(id))||'null'); }catch(e){ planData=null; } const plannedAt = planData && planData.ts ? Number(planData.ts) : null; const card = document.createElement('article'); card.className = 'y8b3s'; card.setAttribute('data-id', id); card.innerHTML = `
${title} – cover image
${level?level:'Course'}
${title}
${provider}
${buildStars(rating)}
${tags.map(t=>`${t}`).join('')}
${lessons?`${lessons} lessons`:'Self-paced'}
${level?level:'All levels'}
${typeof price==='string'?price:('$'+price)}
Planned for ${plannedAt?humanDate(plannedAt):''}--:--:--
${plannedAt?'Session scheduled':'No session scheduled'}
`; frag.appendChild(card); }); wrap.appendChild(frag); renderCounts(); initCardEvents(); tickAllCountdowns(); } function initCardEvents(){ qa('article.y8b3s').forEach(card=>{ const id = card.getAttribute('data-id'); const noteArea = q('textarea', card); const saveBtn = q('[data-save-note]', card); const clearBtn = q('[data-clear-note]', card); const exportBtn = q('[data-export-note]', card); saveBtn.addEventListener('click', ()=>{ const val = noteArea.value.trim(); try{ localStorage.setItem(noteKey(id), val); }catch(e){} saveBtn.textContent='Saved'; setTimeout(()=>saveBtn.textContent='Save note',1200); renderCounts(); }); clearBtn.addEventListener('click', ()=>{ noteArea.value=''; }); exportBtn.addEventListener('click', ()=>{ const payload = { id, note: noteArea.value || '', exportedAt: new Date().toISOString() }; const blob = new Blob([JSON.stringify(payload,null,2)], {type:'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `note-${id}.json`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href), 500); }); const planWrap = q('[data-plan-wrap]', card); const planDate = q('[data-plan-date]', card); const planTime = q('[data-plan-time]', card); const planDur = q('[data-plan-dur]', card); const savePlan = q('[data-save-plan]', card); const cancelPlan = q('[data-cancel-plan]', card); const openPlan = q('[data-open-plan]', card); const info = q('[data-plan-info]', card); const status = q('[data-status]', card); const countdown = q('[data-countdown]', card); openPlan.addEventListener('click', ()=>{ planWrap.scrollIntoView({behavior:'smooth', block:'center'}); planWrap.classList.add('highlightPlan'); setTimeout(()=>planWrap.classList.remove('highlightPlan'), 800); }); savePlan.addEventListener('click', ()=>{ const d = planDate.value; const t = planTime.value; const dur = Number(planDur.value); // Simple validation let valid = true; if(!d){ planDate.style.borderColor='var(--c-danger)'; valid=false; } else { planDate.style.borderColor=''; } if(!t){ planTime.style.borderColor='var(--c-danger)'; valid=false; } else { planTime.style.borderColor=''; } if(!dur || dur<10){ planDur.style.borderColor='var(--c-danger)'; valid=false; } else { planDur.style.borderColor=''; } if(!valid) return; const ts = new Date(`${d}T${t}:00`).getTime(); if(!isFinite(ts) || ts < Date.now()-60*1000){ planDate.style.borderColor='var(--c-warn)'; planTime.style.borderColor='var(--c-warn)'; return; } const data = { ts, dur }; try{ localStorage.setItem(planKey(id), JSON.stringify(data)); }catch(e){} info.style.display=''; q('.due', info).textContent = humanDate(ts); status.textContent = 'Session scheduled'; countdown.textContent = formatDiff(ts - Date.now()); renderCounts(); }); cancelPlan.addEventListener('click', ()=>{ try{ localStorage.removeItem(planKey(id)); }catch(e){} info.style.display='none'; status.textContent = 'No session scheduled'; renderCounts(); }); const removeBtn = q('[data-remove]', card); removeBtn.addEventListener('click', ()=>{ state.removeTarget = id; openModal('#modal-remove'); }); }); } function formatDiff(ms){ if(ms<=0) return '00:00:00'; const sec = Math.floor(ms/1000); const h = String(Math.floor(sec/3600)).padStart(2,'0'); const m = String(Math.floor((sec%3600)/60)).padStart(2,'0'); const s = String(sec%60).padStart(2,'0'); return `${h}:${m}:${s}`; } function tickAllCountdowns(){ qa('article.y8b3s').forEach(card=>{ const id = card.getAttribute('data-id'); let plan=null; try{ plan = JSON.parse(localStorage.getItem(planKey(id))||'null'); }catch(e){ plan=null; } const info = q('[data-plan-info]', card); const countdown = q('[data-countdown]', card); const status = q('[data-status]', card); if(plan && plan.ts){ const diff = plan.ts - Date.now(); if(diff<=0){ info.style.display='none'; status.textContent = 'Overdue session'; status.style.color = 'var(--c-danger)'; }else{ if(info) info.style.display=''; if(countdown) countdown.textContent = formatDiff(diff); } } }); } setInterval(tickAllCountdowns, 1000); // Remove modal function openModal(sel){ const m=q(sel); if(!m) return; m.style.display='flex'; m.setAttribute('aria-hidden','false'); } function closeModal(sel){ const m=q(sel); if(!m) return; m.style.display='none'; m.setAttribute('aria-hidden','true'); } q('[data-close-remove]').addEventListener('click', ()=>closeModal('#modal-remove')); q('#confirmRemove').addEventListener('click', ()=>{ if(!state.removeTarget) return; const id = String(state.removeTarget); state.tracked = state.tracked.filter(x=>String(x)!==id); writeTrackedIds(); try{ localStorage.removeItem(planKey(id)); }catch(e){} closeModal('#modal-remove'); state.removeTarget = null; render(); }); const helpModal = q('#modal-help'); q('#helpOpen').addEventListener('click', ()=>openModal('#modal-help')); q('#helpOpen2').addEventListener('click', ()=>openModal('#modal-help')); q('[data-close-help]').addEventListener('click', ()=>closeModal('#modal-help')); [q('#modal-help'), q('#modal-remove')].forEach(m=>{ m.addEventListener('click', (e)=>{ if(e.target===m) closeModal('#'+m.id); }); m.addEventListener('keydown', (e)=>{ if(e.key==='Escape') closeModal('#'+m.id); }); }); // Filters listeners q('#searchInput').addEventListener('input', (e)=>{ state.filters.search = e.target.value; render(); }); q('#sortSelect').addEventListener('change', (e)=>{ state.filters.sort = e.target.value; render(); }); q('#tagFilter').addEventListener('change', (e)=>{ state.filters.tag = e.target.value; render(); }); // Export all notes/snapshot function downloadJSON(obj, filename){ const blob = new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(a.href), 600); } q('#exportNotes').addEventListener('click', ()=>{ const notes = {}; state.tracked.forEach(id=>{ try{ const n = localStorage.getItem(noteKey(id)); if(n!=null) notes[id]=n; }catch(e){} }); downloadJSON({exportedAt:new Date().toISOString(), notes}, 'tracked-notes.json'); }); q('#exportAll').addEventListener('click', ()=>{ const payload = { exportedAt: new Date().toISOString(), tracked: state.tracked, notes: {}, plans: {}, }; state.tracked.forEach(id=>{ try{ const n = localStorage.getItem(noteKey(id)); if(n!=null) payload.notes[id]=n; const p = localStorage.getItem(planKey(id)); if(p!=null) payload.plans[id]=JSON.parse(p); }catch(e){} }); downloadJSON(payload, 'tracked-snapshot.json'); }); // Clear tracked q('#clearAll').addEventListener('click', ()=>{ if(!state.tracked.length) return; if(confirm('Remove all tracked courses? Notes will remain saved locally.')){ state.tracked = []; writeTrackedIds(); render(); } }); // Streak timer (to midnight) function tickStreak(){ const now = new Date(); const next = new Date(now); next.setHours(24,0,0,0); q('#streakTimer').textContent = formatDiff(next.getTime()-now.getTime()); } tickStreak(); setInterval(tickStreak,1000); // Cookie bar (local preference) const cookieBar = q('#cookieBar'); function loadConsent(){ try{ state.consent = localStorage.getItem('cookieConsent'); }catch(e){ state.consent = null; } if(!state.consent) cookieBar.style.display='block'; } loadConsent(); q('#cookieAccept').addEventListener('click', ()=>{ try{ localStorage.setItem('cookieConsent','accepted'); }catch(e){} cookieBar.style.display='none'; }); q('#cookieDecline').addEventListener('click', ()=>{ try{ localStorage.setItem('cookieConsent','declined'); }catch(e){} cookieBar.style.display='none'; }); // Seed demo q('#seedDemo').addEventListener('click', seedDemo); // Load data function skeleton(n=6){ const wrap = q('#coursesWrap'); wrap.innerHTML = ''; for(let i=0;i
`; wrap.appendChild(sk); } } skeleton(6); fetch('./catalog.json') .then(r=>r.json()) .then(data=>{ state.catalog = Array.isArray(data)? data : (Array.isArray(data.items)? data.items : []); readTrackedIds(); render(); }) .catch(()=>{ // graceful fallback with demo minimal catalog state.catalog = [ {id:'course-101', title:'Conversion Psychology 101', provider:'Norvatrix Academy', level:'Beginner', price:'$49', rating:4.6, lessons:24, tags:['UX','Copy','A/B'], image:'./images/maximum_ditailes_of_this_image.business_people_learning_conversion_psychology_in_modern_classroom_clean_light_colors_high_quality_stock_photo.jpg'}, {id:'course-202', title:'Advanced Funnel Analytics', provider:'Norvatrix Lab', level:'Advanced', price:'$129', rating:4.9, lessons:32, tags:['GA4','Attribution','SQL'], image:'./images/maximum_ditailes_of_this_image.analytics_dashboard_with_charts_and_code_on_dark_monitor_ultra_hd_professional_scene.jpg'}, {id:'course-303', title:'Email Automation Mastery', provider:'Norvatrix Studio', level:'Intermediate', price:'$89', rating:4.7, lessons:18, tags:['ESP','Flows','Segmentation'], image:'./images/maximum_ditailes_of_this_image.email_marketing_flow_diagram_on_laptop_minimal_isometric_3d_render_soft_shadows.jpg'} ]; readTrackedIds(); render(); }); // Keyboard a11y: close modals on Escape globally document.addEventListener('keydown', (e)=>{ if(e.key==='Escape'){ closeModal('#modal-help'); closeModal('#modal-remove'); } }); })();