Ember & Broom

The service shelf

Choose the care your space needs.

Browse focused routines designed for real homes and working spaces. Save a service to Fallows or add it to your planning cart.

`; const footerHTML = ``; document.querySelector('header').innerHTML = headerHTML; document.querySelector('footer').innerHTML = footerHTML; const state = {items: [], filtered: [], page: 1, size: 6}; const $ = id => document.getElementById(id); const fallows = () => JSON.parse(localStorage.getItem('ember-fallows') || '[]'); const cart = () => JSON.parse(localStorage.getItem('ember-cart') || '{}'); function populateCategories() { const cats = [...new Set(state.items.map(i => i.category))]; const sel = $('category'); cats.forEach(c => { const opt = document.createElement('option'); opt.value = c; opt.textContent = c; sel.appendChild(opt); }); } function render() { const searchVal = $('search').value.toLowerCase(); const catVal = $('category').value; state.filtered = state.items.filter(x => { const matchCat = catVal === 'all' || x.category === catVal; const matchSearch = (x.name + ' ' + x.shortDescription + ' ' + x.description).toLowerCase().includes(searchVal); return matchCat && matchSearch; }); const start = (state.page - 1) * state.size; const pageItems = state.filtered.slice(start, start + state.size); $('grid').innerHTML = pageItems.map(x => `

${x.category}

${x.name}

${x.shortDescription}

$${x.price.toFixed(2)} ${x.unit}

`).join(''); const totalPages = Math.ceil(state.filtered.length / state.size) || 1; $('pages').innerHTML = Array.from({length: totalPages}, (_, i) => ``).join(''); } function setupListeners() { $('search').addEventListener('input', () => { state.page = 1; render(); }); $('category').addEventListener('change', () => { state.page = 1; render(); }); document.addEventListener('click', e => { const d = e.target.dataset; if (d.page) { state.page = +d.page; render(); } if (d.follow) { let a = fallows(); if (a.includes(d.follow)) a = a.filter(id => id !== d.follow); else a.push(d.follow); localStorage.setItem('ember-fallows', JSON.stringify(a)); render(); } if (d.cart) { let c = cart(); c[d.cart] = (c[d.cart] || 0) + 1; localStorage.setItem('ember-cart', JSON.stringify(c)); e.target.textContent = 'Added'; setTimeout(() => { e.target.textContent = 'Add to cart'; }, 1200); } if (d.detail) { const x = state.items.find(i => i.id === d.detail); $('detailBody').innerHTML = `

${x.category}

${x.name}

${x.description}

$${x.price.toFixed(2)} ${x.unit} · ${x.duration}

Save to Fallows
`; $('detail').classList.replace('hidden', 'flex'); } if (e.target.id === 'detailClose') $('detail').classList.replace('flex', 'hidden'); }); document.getElementById('detail').addEventListener('click', e => { if (e.target.id === 'detail') $('detail').classList.replace('flex', 'hidden'); if (e.target.dataset.cartModal) { let c = cart(); c[e.target.dataset.cartModal] = (c[e.target.dataset.cartModal] || 0) + 1; localStorage.setItem('ember-cart', JSON.stringify(c)); $('detail').classList.replace('flex', 'hidden'); } }); // header/footer bootstrap const t = document.getElementById('menuToggle'), n = document.getElementById('mobileNav'); t && t.addEventListener('click', () => { const o = !n.classList.contains('hidden'); n.classList.toggle('hidden'); t.setAttribute('aria-expanded', String(!o)); }); const a = document.getElementById('accountModal'); document.getElementById('accountOpen')?.addEventListener('click', () => a.classList.replace('hidden', 'flex')); document.getElementById('accountClose')?.addEventListener('click', () => a.classList.replace('flex', 'hidden')); document.getElementById('themeToggle')?.addEventListener('click', () => { const d = document.documentElement.classList.toggle('dark'); localStorage.setItem('ember-theme', d ? 'dark' : 'light'); }); if (localStorage.getItem('ember-theme') === 'dark') document.documentElement.classList.add('dark'); const banner = document.getElementById('cookieBanner'); if (localStorage.getItem('ember-cookie-consent') === 'accepted') banner?.remove(); document.getElementById('cookieAccept')?.addEventListener('click', () => { localStorage.setItem('ember-cookie-consent', 'accepted'); banner.remove(); }); } fetch('./catalog.json') .then(r => r.json()) .then(data => { state.items = data; populateCategories(); render(); setupListeners(); }) .catch(() => { // fallback with sample data if fetch fails in demo state.items = [ {"id":"s1","name":"Weekly Home Refresh","category":"Home","shortDescription":"Regular maintenance for bright homes.","description":"Complete dusting, vacuuming and surface sanitization every week.","duration":"3 hours","price":185,"unit":"per visit","accent":"#c8875a"}, {"id":"s2","name":"Deep Kitchen Clean","category":"Home","shortDescription":"Focus on the busiest room in the house.","description":"Appliances, behind the stove, inside fridge and cabinets.","duration":"4 hours","price":265,"unit":"per visit","accent":"#5b3a29"}, {"id":"s3","name":"Post-Reno Reset","category":"Specialty","shortDescription":"New construction or renovation dust removal.","description":"Fine dust, paint splatter, adhesive residue, and window frames.","duration":"5 hours","price":310,"unit":"per visit","accent":"#c8875a"}, {"id":"s4","name":"Large Office Tidy","category":"Office","shortDescription":"Corporate and coworking spaces.","description":"Daily or weekly service for desks, meeting rooms and shared areas.","duration":"4 hours","price":240,"unit":"per visit","accent":"#5b3a29"}, {"id":"s5","name":"Move-Out Clean","category":"Specialty","shortDescription":"Deposit-ready results.","description":"Full interior clean for rentals returning keys.","duration":"6 hours","price":385,"unit":"flat","accent":"#c8875a"} ]; populateCategories(); render(); setupListeners(); });