Peteevolution Studio
Join

The course index

A practice with range.

Choose a focused studio experience, save the ones that speak to you, and build a considered route through drawing.

View Fallows

Loading the studio index…

`; const footerHTML = ``; document.getElementById('site-header').innerHTML = headerHTML; document.getElementById('site-footer').innerHTML = footerHTML; const state = {items:[],filtered:[],page:1,size:6}; const getFav = () => JSON.parse(localStorage.getItem('peteevolution-fallows')||'[]'); const saveFav = x => localStorage.setItem('peteevolution-fallows',JSON.stringify(x)); const getCart = () => JSON.parse(localStorage.getItem('peteevolution-cart')||'[]'); const saveCart = x => localStorage.setItem('peteevolution-cart',JSON.stringify(x)); function initTheme() { const btns = document.querySelectorAll('[data-theme-toggle]'); btns.forEach(b => b.onclick = () => { const isDark = document.documentElement.classList.toggle('dark'); localStorage.setItem('peteevolution-theme', isDark ? 'dark' : 'light'); }); } function initModals() { document.querySelectorAll('[data-modal-open]').forEach(b => b.onclick = () => document.getElementById(b.dataset.modalOpen).hidden = false); document.querySelectorAll('[data-modal-close]').forEach(b => b.onclick = () => b.closest('[role=dialog]').hidden = true); } async function init() { initTheme(); initModals(); try { state.items = await (await fetch('./catalog.json')).json(); const cat = document.getElementById('category'); cat.innerHTML = '' + [...new Set(state.items.map(x=>x.category))].map(x=>``).join(''); render(); } catch(e) { document.getElementById('status').textContent = 'The catalog could not be loaded. Please return to the home page.'; } document.getElementById('search').oninput = render; document.getElementById('category').onchange = render; } function filterAndPage() { const q = document.getElementById('search').value.toLowerCase(); const cat = document.getElementById('category').value; let res = state.items.filter(i => { const matchQ = i.title.toLowerCase().includes(q) || i.summary.toLowerCase().includes(q) || i.description.toLowerCase().includes(q); const matchC = cat === 'all' || i.category === cat; return matchQ && matchC; }); state.filtered = res; const totalPages = Math.ceil(res.length / state.size); if (state.page > totalPages) state.page = Math.max(1,totalPages); return res.slice((state.page-1)*state.size, state.page*state.size); } function render() { const grid = document.getElementById('course-grid'); const pag = document.getElementById('pagination'); const status = document.getElementById('status'); const visible = filterAndPage(); if (!visible.length) { grid.innerHTML = ''; status.textContent = 'No courses match your filters.'; pag.innerHTML = ''; return; } status.textContent = `${state.filtered.length} courses available`; grid.innerHTML = visible.map(course => `
${course.category}

${course.title}

${course.level} • ${course.format} • ${course.duration}

${course.summary}

${course.currency}${course.price}
`).join(''); const total = Math.ceil(state.filtered.length / state.size); pag.innerHTML = Array.from({length:total},(_,i)=>i+1).map(i => ``).join(''); } function toggleFav(id) { let favs = getFav(); if (favs.includes(id)) favs = favs.filter(x=>x!==id); else favs.push(id); saveFav(favs); render(); } function addToCart(id) { let cart = getCart(); const existing = cart.find(x=>x.id===id); if (existing) existing.quantity++; else cart.push({id,quantity:1}); saveCart(cart); } function showDetail(id) { const course = state.items.find(x=>x.id===id); if (!course) return; const modal = document.getElementById('detail-modal'); modal.innerHTML = `
${course.category} • ${course.level}

${course.title}

${course.format} • ${course.duration}

${course.description}

Save to Fallows
`; modal.hidden = false; } init();