Welcome back
Sign-in is a local demonstration and does not transmit credentials.
Create your studio profile
Save your preferred courses and return to your practice with less friction.
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.
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}
${course.outcomes.map(o=>`- ${o}
`).join('')}
`;
modal.hidden = false;
}
init();