async function getJSON(path){ const r = await fetch(path); return await r.json(); } // Directory rendering async function renderDirectory(){ const q = (document.getElementById('q')||{}).value?.toLowerCase()||''; const cat = (document.getElementById('cat')||{}).value||'all'; const sort = (document.getElementById('sort')||{}).value||'karma-desc'; const resEl = document.getElementById('results'); if(!resEl) return; const [cats, conf, data] = await Promise.all([ getJSON('./data/categories.json'), getJSON('./data/karma-config.json'), getJSON('./data/listings.json') ]); // populate category select once const catSel = document.getElementById('cat'); if(catSel && catSel.options.length===0){ catSel.append(new Option("All", "all")); cats.categories.forEach(c => catSel.append(new Option(c,c))); } // compute karma for each listing const W = conf.weights; function score(k){ if(!k) return 0; return (k.profile*W.profile + k.reviews*W.reviews + k.velocity*W.velocity + k.verification*W.verification) } let items = data.listings.map(x => ({...x, _karma: score(x.karma)})); // filter if(cat !== 'all') items = items.filter(x => x.category === cat); if(q){ items = items.filter(x => (x.name+x.city+x.category+(x.tags||[]).join(' ')+(x.desc||'')).toLowerCase().includes(q)); } // sort if(sort==='karma-desc') items.sort((a,b)=>b._karma - a._karma); if(sort==='name-asc') items.sort((a,b)=>a.name.localeCompare(b.name)); if(sort==='reviews-desc') items.sort((a,b)=> (b.reviews||0) - (a.reviews||0)); // render resEl.innerHTML = items.map(x => `
${x.name} logo
${x.name}
${x.category} • ${x.city}
Karma ${(x._karma*100).toFixed(0)}
${x.desc||''}
? ${x.stars||'-'} • ${x.reviews||0} reviews ${x.verified? '• Verified':''}
Tags: ${(x.tags||[]).map(t=>`${t}`).join(' ')}
`).join('') || '
No results.
'; } document.addEventListener('DOMContentLoaded', ()=>{ renderDirectory(); renderKarma(); renderResources(); bindCheckout(); }); // Listing JSON generator function generateListingJSON(){ const name = document.getElementById('new_name').value.trim(); const category = document.getElementById('new_category').value.trim(); const city = document.getElementById('new_city').value.trim(); const url = document.getElementById('new_url').value.trim(); const tags = document.getElementById('new_tags').value.split(',').map(s=>s.trim()).filter(Boolean); const desc = document.getElementById('new_desc').value.trim(); const id = name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,''); const obj = { "id": id, "name": name, "category": category, "city": city, "url": url, "tags": tags, "desc": desc, "logo":"./img/placeholder.svg", "reviews": 0, "stars": 0, "verified": false, "karma": {"profile":0.5,"reviews":0.5,"velocity":0.5,"verification":0.0} }; document.getElementById('new_output').value = JSON.stringify(obj, null, 2); } // Digital Karma page async function renderKarma(){ const kconf = document.getElementById('kconf'); const kboard = document.getElementById('kboard'); if(!kconf && !kboard) return; const [conf, data] = await Promise.all([getJSON('./data/karma-config.json'), getJSON('./data/listings.json')]); const W = conf.weights; if(kconf){ kconf.innerHTML = ` SignalWeightDescription Profile${(W.profile*100).toFixed(0)}%Completeness, images, links, schema Reviews${(W.reviews*100).toFixed(0)}%Stars & recent review velocity Velocity${(W.velocity*100).toFixed(0)}%Update cadence / freshness Verification${(W.verification*100).toFixed(0)}%Ownership & identity proof `; } if(kboard){ function score(k){ if(!k) return 0; return (k.profile*W.profile + k.reviews*W.reviews + k.velocity*W.velocity + k.verification*W.verification) } const items = data.listings.map(x=>({...x,_karma:score(x.karma)})).sort((a,b)=>b._karma-a._karma).slice(0,10); kboard.innerHTML = items.map((x,i)=>`
${i+1}. ${x.name} (${x.category})
${(x._karma*100).toFixed(0)}
`).join(''); } } // Health Check logic async function runHealthCheck(){ const [conf] = await Promise.all([getJSON('./data/karma-config.json')]); const W = conf.weights; const gbp = document.getElementById('f_gbp').value==='yes' ? 1 : 0; const cwvSel = document.getElementById('f_cwv').value; const stars = Math.max(1, Math.min(5, parseFloat(document.getElementById('f_stars').value||"0"))); const starsNorm = (stars-1)/(5-1); // 0..1 const reviews = Math.max(0, parseInt(document.getElementById('f_reviews').value||"0",10)); const reviewsNorm = Math.min(1, reviews/25); // cap at 25 in 90 days const last = parseInt(document.getElementById('f_update').value,10); const velocity = last<=30 ? 1 : (last<=90 ? 0.6 : 0.2); const schema = document.getElementById('f_schema').value==='yes' ? 1 : 0; const profile = (gbp*0.5 + schema*0.5); const reviewsSignal = (starsNorm*0.6 + reviewsNorm*0.4); const verification = gbp; const score = (profile*W.profile + reviewsSignal*W.reviews + velocity*W.velocity + verification*W.verification); document.getElementById('hc_score').textContent = `Karma ${(score*100).toFixed(0)}`; const snapshot = { date: new Date().toISOString(), inputs: { gbp: gbp===1, cwv: cwvSel, stars, reviews, lastDays: last, schema: schema===1 }, mapped: { profile, reviewsSignal, velocity, verification }, score: score }; document.getElementById('hc_json').textContent = JSON.stringify(snapshot,null,2); } // Resources async function renderResources(){ const tbl = document.querySelector('#resources_table tbody'); if(!tbl) return; const data = await getJSON('./data/resources.json'); tbl.innerHTML = data.items.map(x=>` ${x.title}${x.type}Open${(x.tags||[]).join(', ')} `).join(''); } // v04: Stripe Checkout wiring async function bindCheckout(){ try{ const map = await getJSON('./data/checkout.json'); document.querySelectorAll('[data-checkout]').forEach(btn=>{ btn.addEventListener('click', ()=>{ const key = btn.getAttribute('data-checkout'); const prod = map.products && map.products[key]; if(prod && prod.checkout_url && !prod.checkout_url.includes('REPLACE_ME')){ window.location.href = prod.checkout_url; }else{ alert('Checkout not configured yet. Replace the checkout_url in /data/checkout.json for \"'+key+'\".'); } }); }); }catch(e){ console.warn('Checkout map missing', e); } } //Toggle Mobile Menu document.addEventListener('DOMContentLoaded', () => { const toggle = document.getElementById('nav-toggle'); const menu = document.getElementById('nav-menu'); if (!toggle || !menu) return; toggle.addEventListener('click', (e) => { e.preventDefault(); const isOpen = menu.classList.toggle('open'); // matches your CSS toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); }); // optional niceties: menu.addEventListener('click', (e) => { if (e.target.closest('a')) { menu.classList.remove('open'); toggle.setAttribute('aria-expanded', 'false'); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { menu.classList.remove('open'); toggle.setAttribute('aria-expanded', 'false'); } }); });