Fallows

Saved courses you want to revisit. Remove items, preview details, or add directly to the cart.

Browse catalog
Saved: 0
In cart: 0
    Tip Use Search to quickly filter saved courses.
    Go to cart

    What you get

    Quick facts

    Level
    Duration
    Updated

    Price

    Support

    Questions? Call +1 (555) 123-9476

    rapidzone.click — secure checkout and instant access.

    How Fallows works

    Everything is stored locally in your browser.

    Saved list

    We read favorite course IDs from localStorage and match them against catalog.json.

    Quick actions

    Use Add to cart to save time. Removing from fallows updates instantly.

    Privacy

    No account required. If you clear browser storage, fallows and cart may reset.

    Open catalog
    `; } function wireHeaderFeatures(){ const root = document.documentElement; const themeKey = 'theme'; const saved = localStorage.getItem(themeKey); const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const initial = saved || (prefersDark ? 'dark' : 'light'); applyTheme(initial); function applyTheme(mode){ const dark = mode === 'dark'; root.classList.toggle('dark', dark); if(dark){ document.body.classList.remove('bg-white','text-gray-900'); document.body.classList.add('bg-slate-950','text-slate-100'); }else{ document.body.classList.remove('bg-slate-950','text-slate-100'); document.body.classList.add('bg-white','text-gray-900'); } localStorage.setItem(themeKey, mode); syncDialogTheme(dark); } function syncDialogTheme(dark){ const dialogs = qsa('dialog'); for(const d of dialogs){ const inner = d.firstElementChild; if(!inner) continue; if(dark){ inner.classList.remove('bg-white','text-gray-900','border-gray-200'); inner.classList.add('bg-slate-900','text-slate-100','border-slate-700'); qsa('.bg-gray-50', inner).forEach(el=>{ el.classList.remove('bg-gray-50'); el.classList.add('bg-slate-950'); }); qsa('.border-gray-200', inner).forEach(el=>{ el.classList.remove('border-gray-200'); el.classList.add('border-slate-700'); }); qsa('.text-gray-700', inner).forEach(el=>{ el.classList.remove('text-gray-700'); el.classList.add('text-slate-300'); }); qsa('.text-gray-600', inner).forEach(el=>{ el.classList.remove('text-gray-600'); el.classList.add('text-slate-300'); }); qsa('.text-gray-500', inner).forEach(el=>{ el.classList.remove('text-gray-500'); el.classList.add('text-slate-400'); }); qsa('.text-gray-800', inner).forEach(el=>{ el.classList.remove('text-gray-800'); el.classList.add('text-slate-200'); }); qsa('.text-gray-900', inner).forEach(el=>{ el.classList.remove('text-gray-900'); el.classList.add('text-slate-100'); }); }else{ inner.classList.remove('bg-slate-900','text-slate-100','border-slate-700'); inner.classList.add('bg-white','text-gray-900','border-gray-200'); } } } const btn = qs('[data-theme-toggle]') || qs('#theme-toggle') || qs('#toggle-theme'); if(btn){ btn.addEventListener('click', ()=>{ const next = (localStorage.getItem(themeKey) === 'dark') ? 'light' : 'dark'; applyTheme(next); }, {passive:true}); } const cookieKey = 'cookieConsent'; if(!localStorage.getItem(cookieKey)){ createCookieBanner(cookieKey); } function createCookieBanner(key){ const wrap = document.createElement('section'); wrap.setAttribute('aria-label','Cookie banner'); wrap.className = 'fixed bottom-4 left-4 right-4 z-50'; wrap.innerHTML = `
    C

    Cookies & local storage

    We store fallows and cart locally to keep your selections. You can change this anytime by clearing browser data.

    `; document.body.appendChild(wrap); const accept = qs('[data-cookie-accept]', wrap); const close = qs('[data-cookie-close]', wrap); const remove = (val)=>{ localStorage.setItem(key, val); wrap.style.opacity = '0'; wrap.style.transform = 'translateY(8px)'; setTimeout(()=>wrap.remove(), 220); }; accept.addEventListener('click', ()=>remove('accepted')); close.addEventListener('click', ()=>remove('declined')); } } async function loadCatalog(){ let data = null; try{ const r = await fetch(d9t1q.catalogUrl, {cache:'no-store'}); if(!r.ok) throw new Error('Bad response'); data = await r.json(); }catch(e){ data = []; } const arr = Array.isArray(data) ? data : (data && Array.isArray(data.items) ? data.items : (data && Array.isArray(data.catalog) ? data.catalog : [])); p8x0v.catalog = arr; p8x0v.catalogMap = new Map(); for(const it of arr){ if(!it) continue; const id = normalizeId(it.id ?? it.courseId ?? it.slug ?? it.sku ?? it.uid); if(!id) continue; p8x0v.catalogMap.set(id, it); } } function getCourseTitle(c){ return safeText(c.title ?? c.name ?? c.h1 ?? c.courseTitle ?? 'Course'); } function getCourseDesc(c){ return safeText(c.description ?? c.desc ?? c.summary ?? c.about ?? ''); } function getCourseLevel(c){ return safeText(c.level ?? c.difficulty ?? c.skill ?? 'All levels'); } function getCourseDuration(c){ const v = c.duration ?? c.length ?? c.time ?? c.hours; if(v === undefined || v === null || v === '') return 'Flexible'; return safeText(v); } function getCourseUpdated(c){ const v = c.updated ?? c.lastUpdated ?? c.updatedAt ?? c.date; if(!v) return 'Recently'; return safeText(v); } function getCoursePriceNumber(c){ const raw = c.price ?? c.cost ?? c.amount ?? c.usd ?? 0; const n = typeof raw === 'string' ? Number(raw.replace(/[^\d.]/g,'')) : Number(raw); return Number.isFinite(n) ? n : 0; } function getCourseTags(c){ const t = c.tags ?? c.tag ?? c.categories ?? c.category ?? []; const arr = asArray(t).map(safeText).map(s=>s.trim()).filter(Boolean); return arr.slice(0, 6); } function render(){ p8x0v.favIds = readFavIds(); p8x0v.cartIds = readCartIds(); qs('#count-saved').textContent = String(p8x0v.favIds.length); qs('#count-cart').textContent = String(p8x0v.cartIds.length); const query = (qs('#search').value || '').trim().toLowerCase(); const sort = qs('#sort').value; const list = qs('#fav-list'); list.innerHTML = ''; const resolved = p8x0v.favIds.map((id, idx)=>{ const c = p8x0v.catalogMap.get(id); return {id, idx, c}; }); let filtered = resolved.filter(x=>{ if(!query) return true; const c = x.c; const hay = [ x.id, c ? getCourseTitle(c) : '', c ? getCourseDesc(c) : '', c ? getCourseLevel(c) : '', ...(c ? getCourseTags(c) : []) ].join(' ').toLowerCase(); return hay.includes(query); }); if(sort === 'title'){ filtered.sort((a,b)=>{ const at = (a.c ? getCourseTitle(a.c) : a.id).toLowerCase(); const bt = (b.c ? getCourseTitle(b.c) : b.id).toLowerCase(); return at.localeCompare(bt); }); }else if(sort === 'priceLow'){ filtered.sort((a,b)=> (a.c?getCoursePriceNumber(a.c):0) - (b.c?getCoursePriceNumber(b.c):0)); }else if(sort === 'priceHigh'){ filtered.sort((a,b)=> (b.c?getCoursePriceNumber(b.c):0) - (a.c?getCoursePriceNumber(a.c):0)); }else{ filtered.sort((a,b)=> a.idx - b.idx); } const state = qs('#state-bar'); const clearAllBtn = qs('#btn-clear-all'); if(p8x0v.favIds.length === 0){ state.classList.remove('hidden'); qs('#state-title').textContent = 'Nothing saved yet'; qs('#state-desc').textContent = 'Add courses to fallows from the catalog to see them here.'; clearAllBtn.disabled = true; clearAllBtn.classList.add('opacity-50'); }else if(filtered.length === 0){ state.classList.remove('hidden'); qs('#state-title').textContent = 'No matches'; qs('#state-desc').textContent = 'Try a different search or change sorting.'; clearAllBtn.disabled = false; clearAllBtn.classList.remove('opacity-50'); }else{ state.classList.add('hidden'); clearAllBtn.disabled = false; clearAllBtn.classList.remove('opacity-50'); } for(const item of filtered){ const li = document.createElement('li'); li.className = 'a7n0p rounded-2xl border border-gray-200 bg-white/80 t5m1y shadow-soft hover:shadow-md overflow-hidden flex flex-col'; li.dataset.courseId = item.id; const c = item.c; const title = c ? getCourseTitle(c) : `Course ID: ${item.id}`; const desc = c ? getCourseDesc(c) : 'This saved item is not present in the current catalog.json. You can remove it from fallows.'; const level = c ? getCourseLevel(c) : 'Unknown level'; const price = c ? money(getCoursePriceNumber(c)) : money(0); const tags = c ? getCourseTags(c) : []; const inCart = p8x0v.cartIds.includes(item.id); li.innerHTML = `

    Saved course

    ${escapeHtml(title)}

    ${escapeHtml(price)}
    ${escapeHtml(level)}

    ${escapeHtml(desc)}

    ${tags.map(t=>`${escapeHtml(t)}`).join('')}
    ID: ${escapeHtml(item.id)} ${inCart ? 'Ready to checkout' : 'Quick add available'}
    `; list.appendChild(li); } } function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } function showFeedback(title, sub, tip){ qs('#modal-feedback-title').textContent = title || 'Updated'; qs('#modal-feedback-sub').textContent = sub || 'Your fallows have been updated.'; qs('#modal-feedback-tip').textContent = tip || 'Use Search to quickly filter saved courses.'; openModal('#modal-feedback'); } function removeFav(id){ const next = p8x0v.favIds.filter(x => normalizeId(x) !== normalizeId(id)); writeFavIds(next); qs('#count-saved').textContent = String(next.length); render(); showFeedback('Removed', 'This course has been removed from your fallows.', 'You can re-add it from the catalog anytime.'); } function addToCart(id){ const nid = normalizeId(id); if(!nid) return; const next = uniq([...p8x0v.cartIds, nid]); writeCartIds(next); qs('#count-cart').textContent = String(next.length); render(); showFeedback('Added to cart', 'Course added to your cart.', 'Proceed to cart when you are ready.'); } function previewCourse(id){ const nid = normalizeId(id); const c = p8x0v.catalogMap.get(nid); p8x0v.activeCourseId = nid; const title = c ? getCourseTitle(c) : `Course ${nid}`; const desc = c ? getCourseDesc(c) : 'This item is not present in the current catalog.json. You can remove it from fallows or keep it saved.'; const level = c ? getCourseLevel(c) : 'Unknown'; const duration = c ? getCourseDuration(c) : 'Unknown'; const updated = c ? getCourseUpdated(c) : 'Unknown'; const price = c ? money(getCoursePriceNumber(c)) : money(0); qs('#modal-course-title').textContent = title; qs('#modal-course-meta').textContent = `ID: ${nid} • Level: ${level}`; qs('#modal-course-desc').textContent = desc; qs('#modal-course-level').textContent = level; qs('#modal-course-duration').textContent = duration; qs('#modal-course-updated').textContent = updated; qs('#modal-course-price').textContent = price; const bullets = qs('#modal-course-bullets'); bullets.innerHTML = ''; const tags = c ? getCourseTags(c) : []; const priceN = c ? getCoursePriceNumber(c) : 0; const items = [ `Curated knitting exercises and examples`, `Self-paced lessons with practical checkpoints`, `Saved locally in your browser for quick access`, tags.length ? `Tags: ${tags.join(', ')}` : `Works with any yarn weight and needle size`, priceN > 0 ? `One-time purchase with instant access` : `Free or included access` ]; for(const t of items){ const li = document.createElement('li'); li.className = 'flex items-start gap-2'; li.innerHTML = `${escapeHtml(t)}`; bullets.appendChild(li); } const inCart = p8x0v.cartIds.includes(nid); const addBtn = qs('#modal-course-add'); addBtn.textContent = inCart ? 'In cart' : 'Add to cart'; addBtn.classList.toggle('bg-emerald-600', inCart); addBtn.classList.toggle('hover:bg-emerald-700', inCart); addBtn.classList.toggle('bg-accent', !inCart); addBtn.classList.toggle('hover:bg-accent-800', !inCart); addBtn.disabled = inCart; addBtn.classList.toggle('opacity-70', inCart); openModal('#modal-course'); } function openConfirm(title, desc, yesText, action){ p8x0v.confirmAction = action; qs('#modal-confirm-title').textContent = title || 'Confirm'; qs('#modal-confirm-desc').textContent = desc || 'Are you sure?'; qs('#modal-confirm-yes').textContent = yesText || 'Yes, do it'; openModal('#modal-confirm'); } function closeAllDialogs(){ qsa('dialog[open]').forEach(d=>{ try{ d.close(); } catch(e){} }); } function wireUI(){ qsa('[data-modal-close]').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const d = e.target.closest('dialog'); if(d) closeModal(d); }); }); qsa('dialog').forEach(d=>{ d.addEventListener('click', (e)=>{ const rect = d.getBoundingClientRect(); const inDialog = (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom); if(!inDialog){ try{ d.close(); }catch(err){} } }); d.addEventListener('cancel', (e)=>{ e.preventDefault(); try{ d.close(); }catch(err){} }); }); qs('#btn-refresh').addEventListener('click', async ()=>{ await loadCatalog(); render(); showFeedback('Refreshed', 'Latest catalog.json has been loaded.', 'If something is missing, it may have been removed from the catalog.'); }); qs('#btn-open-help').addEventListener('click', ()=>openModal('#modal-help')); qs('#btn-clear-all').addEventListener('click', ()=>{ if(p8x0v.favIds.length === 0) return; openConfirm( 'Clear fallows', `This will remove ${p8x0v.favIds.length} saved course(s) from this browser.`, 'Yes, clear all', ()=>{ writeFavIds([]); render(); showFeedback('Cleared', 'All fallows have been removed.', 'Browse the catalog to add favorites again.'); } ); }); qs('#modal-confirm-yes').addEventListener('click', ()=>{ const action = p8x0v.confirmAction; p8x0v.confirmAction = null; closeModal(qs('#modal-confirm')); if(typeof action === 'function') action(); }); qs('#search').addEventListener('input', ()=>{ render(); }); qs('#sort').addEventListener('change', ()=>{ render(); }); qs('#fav-list').addEventListener('click', async (e)=>{ const btn = e.target.closest('button'); if(!btn) return; const li = e.target.closest('li[data-course-id]'); if(!li) return; const id = normalizeId(li.dataset.courseId); const act = btn.dataset.action; if(act === 'remove'){ openConfirm( 'Remove from fallows', 'This course will be removed from your saved list.', 'Yes, remove', ()=>removeFav(id) ); }else if(act === 'addcart'){ addToCart(id); }else if(act === 'preview'){ previewCourse(id); }else if(act === 'copyid'){ try{ await navigator.clipboard.writeText(id); showFeedback('Copied', 'Course ID copied to clipboard.', id); }catch(err){ showFeedback('Copy failed', 'Clipboard permission not available in this browser.', `ID: ${id}`); } } }); qs('#modal-course-add').addEventListener('click', ()=>{ if(!p8x0v.activeCourseId) return; addToCart(p8x0v.activeCourseId); const inCart = p8x0v.cartIds.includes(p8x0v.activeCourseId); if(inCart){ const addBtn = qs('#modal-course-add'); addBtn.textContent = 'In cart'; addBtn.disabled = true; addBtn.classList.add('opacity-70','bg-emerald-600','hover:bg-emerald-700'); addBtn.classList.remove('bg-accent','hover:bg-accent-800'); } }); qs('#modal-course-remove').addEventListener('click', ()=>{ if(!p8x0v.activeCourseId) return; const id = p8x0v.activeCourseId; openConfirm( 'Remove from fallows', 'This course will be removed from your saved list.', 'Yes, remove', ()=>{ removeFav(id); closeAllDialogs(); } ); }); window.addEventListener('storage', (e)=>{ if([p8x0v.favKey, p8x0v.cartKey].includes(e.key)){ render(); } }); } (async function init(){ await loadComponents(); await loadCatalog(); wireUI(); render(); })();