Consistentlio Craftworks
The Drainmaker’s Field Note

Course Catalog

Practical training for cleaner drains and more confident work.

Editorial Note

Every course is organized around four phases: observation, access, action, and cleanup. Filters reflect these principles.

Learn our method →
Reset
Showing 0 courses
Cart: 0 items
`; const footerHTML = ` `; document.querySelector('header').innerHTML = headerHTML; document.querySelector('footer').innerHTML = footerHTML; } // Theme toggle function initThemeToggle() { const toggle = document.querySelector('[data-theme-toggle]'); if (!toggle) return; const savedTheme = localStorage.getItem('theme'); if (savedTheme === 'dark') document.documentElement.classList.add('dark'); toggle.addEventListener('click', () => { document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light'); }); } // Auth modal function initAuthModal() { const modal = document.querySelector('[data-auth-modal]'); if (!modal) return; document.querySelectorAll('[data-auth]').forEach(btn => { btn.addEventListener('click', () => { modal.hidden = false; const tab = btn.getAttribute('data-auth'); switchAuthTab(tab); }); }); modal.querySelector('[data-close-modal]').addEventListener('click', () => modal.hidden = true); modal.addEventListener('click', e => { if (e.target === modal) modal.hidden = true; }); const loginBtn = modal.querySelector('[data-tab="login"]'); const registerBtn = modal.querySelector('[data-tab="register"]'); loginBtn.addEventListener('click', () => switchAuthTab('login')); registerBtn.addEventListener('click', () => switchAuthTab('register')); } function switchAuthTab(tab) { const modal = document.querySelector('[data-auth-modal]'); const loginPanel = modal.querySelector('[data-panel="login"]'); const registerPanel = modal.querySelector('[data-panel="register"]'); const loginTab = modal.querySelector('[data-tab="login"]'); const registerTab = modal.querySelector('[data-tab="register"]'); if (tab === 'login') { loginPanel.hidden = false; registerPanel.hidden = true; loginTab.classList.add('border-b-2', 'border-amber-950'); registerTab.classList.remove('border-b-2', 'border-amber-950'); registerTab.classList.add('text-stone-500'); } else { loginPanel.hidden = true; registerPanel.hidden = false; registerTab.classList.add('border-b-2', 'border-amber-950'); loginTab.classList.remove('border-b-2', 'border-amber-950'); loginTab.classList.add('text-stone-500'); } } // Mobile menu function initMobileMenu() { const btn = document.querySelector('[data-mobile-menu]'); const panel = document.querySelector('[data-mobile-panel]'); if (!btn || !panel) return; btn.addEventListener('click', () => { const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', !expanded); panel.hidden = expanded; }); } // Catalog logic let allProducts = []; let currentPage = 1; const itemsPerPage = 6; let filteredProducts = []; function updateCartCount() { const cart = JSON.parse(localStorage.getItem('consistentlio-cart') || '{}'); const count = Object.values(cart).reduce((sum, qty) => sum + qty, 0); const el = document.getElementById('cart-count'); if (el) el.textContent = `Cart: ${count} items`; } function updateFavorites() { // Handled per card } async function fetchCatalog() { try { const res = await fetch('./catalog.json'); if (!res.ok) throw new Error('Failed to fetch'); allProducts = await res.json(); populateFilters(); filterAndRender(); updateCartCount(); } catch (err) { document.getElementById('error-panel').classList.remove('hidden'); document.getElementById('products-grid').innerHTML = ''; } } function populateFilters() { const categories = [...new Set(allProducts.map(p => p.category))]; const levels = [...new Set(allProducts.map(p => p.level))]; const catSelect = document.getElementById('category-filter'); const lvlSelect = document.getElementById('level-filter'); categories.forEach(cat => { const opt = document.createElement('option'); opt.value = cat; opt.textContent = cat; catSelect.appendChild(opt); }); levels.forEach(lvl => { const opt = document.createElement('option'); opt.value = lvl; opt.textContent = lvl; lvlSelect.appendChild(opt); }); } function filterAndRender() { const search = document.getElementById('search-input').value.toLowerCase(); const category = document.getElementById('category-filter').value; const level = document.getElementById('level-filter').value; const sort = document.getElementById('sort-select').value; filteredProducts = allProducts.filter(product => { const matchesSearch = product.title.toLowerCase().includes(search) || product.category.toLowerCase().includes(search) || product.shortDescription.toLowerCase().includes(search); const matchesCategory = !category || product.category === category; const matchesLevel = !level || product.level === level; return matchesSearch && matchesCategory && matchesLevel; }); // Sorting if (sort === 'title-asc') { filteredProducts.sort((a, b) => a.title.localeCompare(b.title)); } else if (sort === 'price-asc') { filteredProducts.sort((a, b) => a.price - b.price); } else if (sort === 'price-desc') { filteredProducts.sort((a, b) => b.price - a.price); } renderProducts(); renderPagination(); } function renderProducts() { const grid = document.getElementById('products-grid'); grid.innerHTML = ''; const start = (currentPage - 1) * itemsPerPage; const end = start + itemsPerPage; const pageProducts = filteredProducts.slice(start, end); const resultsEl = document.getElementById('results-count'); resultsEl.textContent = `Showing ${pageProducts.length} of ${filteredProducts.length} courses`; if (pageProducts.length === 0) { grid.innerHTML = `
No courses found matching your criteria.
`; return; } const fallows = JSON.parse(localStorage.getItem('consistentlio-fallows') || '[]'); pageProducts.forEach(product => { const isFav = fallows.includes(product.id); const card = document.createElement('div'); card.className = `dd23l xk9h7 p2aql card-hover flex flex-col rounded-3xl border bg-white p-6`; card.innerHTML = `
${product.category}

${product.title}

${product.level} ${product.format} ${product.duration}

${product.shortDescription}

$${product.price}
`; grid.appendChild(card); }); attachCardListeners(); } function attachCardListeners() { // Favorites document.querySelectorAll('[data-favorite]').forEach(btn => { btn.addEventListener('click', () => { const id = btn.getAttribute('data-favorite'); let fallows = JSON.parse(localStorage.getItem('consistentlio-fallows') || '[]'); if (fallows.includes(id)) { fallows = fallows.filter(i => i !== id); btn.classList.remove('text-amber-700'); btn.classList.add('text-stone-300'); } else { fallows.push(id); btn.classList.add('text-amber-700'); btn.classList.remove('text-stone-300'); } localStorage.setItem('consistentlio-fallows', JSON.stringify(fallows)); }); }); // Details document.querySelectorAll('[data-details]').forEach(btn => { btn.addEventListener('click', () => { const id = btn.getAttribute('data-details'); const product = allProducts.find(p => p.id === id); if (product) showDetailModal(product); }); }); // Add to cart document.querySelectorAll('[data-cart]').forEach(btn => { btn.addEventListener('click', () => { const id = btn.getAttribute('data-cart'); let cart = JSON.parse(localStorage.getItem('consistentlio-cart') || '{}'); cart[id] = (cart[id] || 0) + 1; localStorage.setItem('consistentlio-cart', JSON.stringify(cart)); updateCartCount(); btn.textContent = 'Added!'; setTimeout(() => { btn.textContent = 'Add to cart'; }, 1200); }); }); } function renderPagination() { const container = document.getElementById('pagination'); container.innerHTML = ''; const totalPages = Math.ceil(filteredProducts.length / itemsPerPage); if (totalPages <= 1) return; const prev = document.createElement('button'); prev.textContent = '←'; prev.className = `px-4 py-2 text-sm ${currentPage === 1 ? 'text-stone-300' : 'hover:text-amber-700'}`; prev.disabled = currentPage === 1; prev.addEventListener('click', () => { currentPage--; filterAndRender(); }); container.appendChild(prev); for (let i = 1; i <= totalPages; i++) { const btn = document.createElement('button'); btn.textContent = i; btn.className = `px-4 py-2 text-sm rounded-full ${i === currentPage ? 'bg-amber-950 text-stone-50' : 'hover:bg-stone-100'}`; btn.addEventListener('click', () => { currentPage = i; filterAndRender(); }); container.appendChild(btn); } const next = document.createElement('button'); next.textContent = '→'; next.className = `px-4 py-2 text-sm ${currentPage === totalPages ? 'text-stone-300' : 'hover:text-amber-700'}`; next.disabled = currentPage === totalPages; next.addEventListener('click', () => { currentPage++; filterAndRender(); }); container.appendChild(next); } function showDetailModal(product) { const modal = document.getElementById('detail-modal'); document.getElementById('modal-category').textContent = product.category; document.getElementById('modal-title').textContent = product.title; document.getElementById('modal-level').textContent = product.level; document.getElementById('modal-format').textContent = product.format; document.getElementById('modal-duration').textContent = product.duration; document.getElementById('modal-lessons').textContent = product.lessonCount; document.getElementById('modal-description').textContent = product.description; document.getElementById('modal-price').textContent = `$${product.price}`; const outcomesList = document.getElementById('modal-outcomes'); outcomesList.innerHTML = ''; product.outcomes.forEach(outcome => { const li = document.createElement('li'); li.className = 'flex items-start gap-2'; li.innerHTML = ` ${outcome}`; outcomesList.appendChild(li); }); const cartLink = document.getElementById('modal-cart-link'); cartLink.onclick = () => { let cart = JSON.parse(localStorage.getItem('consistentlio-cart') || '{}'); cart[product.id] = (cart[product.id] || 0) + 1; localStorage.setItem('consistentlio-cart', JSON.stringify(cart)); updateCartCount(); }; modal.hidden = false; const closeBtn = document.getElementById('modal-close'); const closeModal = () => { modal.hidden = true; }; closeBtn.onclick = closeModal; modal.onclick = (e) => { if (e.target === modal) closeModal(); }; document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', esc); } }, { once: true }); } function initCatalogControls() { const search = document.getElementById('search-input'); const cat = document.getElementById('category-filter'); const lvl = document.getElementById('level-filter'); const sort = document.getElementById('sort-select'); [search, cat, lvl, sort].forEach(el => { el.addEventListener('input', () => { currentPage = 1; filterAndRender(); }); el.addEventListener('change', () => { currentPage = 1; filterAndRender(); }); }); } function initCookieBanner() { const banner = document.querySelector('[data-cookie-banner]'); if (!banner) return; const key = 'consistentlio-cookie-consent'; if (localStorage.getItem(key)) { banner.style.display = 'none'; return; } banner.style.display = 'flex'; banner.querySelector('[data-cookie-accept]').addEventListener('click', () => { banner.style.display = 'none'; localStorage.setItem(key, 'true'); }); banner.querySelector('[data-cookie-dismiss]').addEventListener('click', () => { banner.style.display = 'none'; localStorage.setItem(key, 'true'); }); } // Main initialization async function initialize() { initializeTailwind(); await loadSharedComponents(); initThemeToggle(); initAuthModal(); initMobileMenu(); initCatalogControls(); initCookieBanner(); await fetchCatalog(); } document.addEventListener('DOMContentLoaded', initialize);