Daily Japa

#japa-app .card{border:1px solid #e5e7eb;border-radius:16px;padding:16px;box-shadow:0 1px 2px rgba(0,0,0,.04)} #japa-app h2{margin:.2rem 0 1rem;font-size:1.3rem} #japa-app label{display:block;font-size:.9rem;margin:.5rem 0 .25rem} #japa-app input, #japa-app select, #japa-app button{ width:100%;padding:.65rem .75rem;border:1px solid #d1d5db;border-radius:12px;font-size:1rem } #japa-app .row{display:grid;grid-template-columns:1fr 1fr;gap:12px} #japa-app .row-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px} #japa-app .btn{cursor:pointer;border:0;border-radius:9999px;padding:.8rem 1rem;font-weight:600} #japa-app .btn-primary{background:#111827;color:white} #japa-app .btn-ghost{background:white;border:1px solid #d1d5db} #japa-app .counter{display:flex;gap:12px;align-items:center;justify-content:center;margin:12px 0} #japa-app .big{font-size:2.6rem;font-weight:800} #japa-app .pill{display:inline-block;background:#f3f4f6;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .6rem;margin:.2rem .25rem;font-size:.85rem;cursor:pointer} #japa-app .pill.active{background:#111827;color:#fff;border-color:#111827} #japa-app table{width:100%;border-collapse:collapse;margin-top:8px} #japa-app th, #japa-app td{border-bottom:1px solid #eee;padding:.55rem .5rem;text-align:left;font-size:.92rem} #japa-app .muted{color:#6b7280;font-size:.9rem} #japa-app .grid{display:grid;gap:16px} #japa-app .tabs{display:flex;gap:8px;margin:8px 0 0} #japa-app .tab{padding:.5rem .8rem;border:1px solid #d1d5db;border-radius:999px;cursor:pointer} #japa-app .tab.active{background:#111827;color:#fff;border-color:#111827} #japa-app .hidden{display:none}

🧘🏽‍♂️ Japa Counter (Subscribers of selfdiscovery.uk)

Your Name
Your Email (for your history)
Mantra
Target Count (per round)
27 54 108 Custom
0 / 108
+
Tap to Count Japa
Reset Log Completed Round
Today
Leaderboard
My History

📅 Today’s Japa

Loading…
// ====== CONFIG ====== // Optional: connect to Google Apps Script Web App for shared storage. const GAS_ENDPOINT = “”; // paste your Apps Script Web App URL (Step 2). Leave blank for per-device local mode. // ====== HELPERS ====== const $ = (id)=>document.getElementById(id); const statusEl = $(“jp-status”); const nameEl = $(“jp-name”); const emailEl = $(“jp-email”); const mantraEl = $(“jp-mantra”); const countEl = $(“jp-count”); const targetDisplayEl = $(“jp-target-display”); const customTargetEl = $(“jp-target”); const dateEl = $(“jp-date”); function todayParts(){ const now = new Date(); const dayName = now.toLocaleDateString(undefined,{weekday:”long”}); const dateStr = now.toLocaleDateString(undefined,{year:”numeric”,month:”long”,day:”numeric”}); const year = now.getFullYear(); return { dayName, dateStr, year, iso: now.toISOString().slice(0,10) }; } function toast(msg, ok=true){ statusEl.textContent = msg; statusEl.style.color = ok ? “#16a34a” : “#b91c1c”; clearTimeout(window.__jp_to); window.__jp_to = setTimeout(()=>{ statusEl.textContent=””; }, 3000); } function getTarget(){ const active = document.querySelector(‘#japa-app .pill.active’); const v = active?.dataset?.target||”108″; return v===”custom” ? Math.max(1, parseInt(customTargetEl.value||”108″,10)) : parseInt(v,10); } function setTarget(val){ document.querySelectorAll(‘#japa-app .pill’).forEach(p=>p.classList.remove(‘active’)); let pill = document.querySelector(`#japa-app .pill[data-target=”${val}”]`); if(!pill){ pill = document.querySelector(`#japa-app .pill[data-target=”custom”]`); customTargetEl.style.display=”inline-block”; customTargetEl.value = val; } pill.classList.add(‘active’); customTargetEl.style.display = pill.dataset.target===”custom” ? “inline-block” : “none”; targetDisplayEl.textContent = getTarget(); savePrefs(); } function renderTable(rows, emptyText){ if(!rows || !rows.length) return `
${emptyText}
`; const head = `DateNameMantraCount`; const body = rows.map(r=>` ${r.day} ${r.date} ${r.year} ${escapeHtml(r.name||”Anonymous”)} ${escapeHtml(r.mantra||”—”)} ${r.count|0} `).join(“”); return `${head}${body}
`; } function renderLeaderboard(rows){ if(!rows || !rows.length) return `
No entries yet.
`; const totals = {}; rows.forEach(r=>{ const n = (r.name||”Anonymous”).trim(); totals[n] = (totals[n]||0) + (r.count|0); }); const sorted = Object.entries(totals).map(([name,total])=>({name,total})).sort((a,b)=>b.total-a.total); const head = `RankNameTotal Count`; const body = sorted.map((r,i)=>`${i+1}${escapeHtml(r.name)}${r.total}`).join(“”); return `${head}${body}
`; } function escapeHtml(s){ return String(s||””).replace(/[&”‘]/g, m => ({‘&’:’&’,”:’>’,'”‘:’"’,”‘”:’'’}[m])); } function sumBy(arr, key){ return arr.reduce((a,b)=>a+(+b[key]||0),0); } // ====== STATE & PREFS ====== let count = 0; function setCount(v){ count = Math.max(0, v|0); countEl.textContent = count; } function savePrefs(){ localStorage.setItem(“japa_prefs”, JSON.stringify({ name: nameEl.value.trim(), email: emailEl.value.trim().toLowerCase(), mantra: mantraEl.value.trim(), target: getTarget() })); } function loadPrefs(){ try{ const p = JSON.parse(localStorage.getItem(“japa_prefs”)||”{}”); if(p.name) nameEl.value = p.name; if(p.email) emailEl.value = p.email; if(p.mantra) mantraEl.value = p.mantra; if(p.target) setTarget(p.target); }catch(e){} } // ====== STORAGE LAYER ====== async function fetchAll(){ if(GAS_ENDPOINT){ const r = await fetch(GAS_ENDPOINT + “?mode=list”, {method:”GET”}); if(!r.ok) throw new Error(“Network error”); const data = await r.json(); return data.rows || []; }else{ return JSON.parse(localStorage.getItem(“japa_entries”)||”[]”); } } async function saveEntry(entry){ if(GAS_ENDPOINT){ const r = await fetch(GAS_ENDPOINT, { method:”POST”, headers:{ “Content-Type”:”application/json” }, body: JSON.stringify(entry) }); if(!r.ok) throw new Error(“Save failed”); return await r.json(); }else{ const arr = JSON.parse(localStorage.getItem(“japa_entries”)||”[]”); arr.push(entry); localStorage.setItem(“japa_entries”, JSON.stringify(arr)); return { ok:true }; } } async function refreshViews(){ const all = await fetchAll().catch(()=>[]); const t = todayParts(); const todays = all.filter(r=>r.iso===t.iso); $(“today-table”).innerHTML = renderTable(todays, “No entries yet today.”); $(“alltime-table”).innerHTML = renderLeaderboard(all); const myEmail = (emailEl.value||””).trim().toLowerCase(); if(!myEmail){ $(“my-table”).innerHTML = “Enter your email above to see your logs.”; return; } const mine = all.filter(r=>(r.email||””).toLowerCase()===myEmail); if(!mine.length){ $(“my-table”).innerHTML = “No logs found for your email yet.”; return; } const total = sumBy(mine,”count”); $(“my-table”).innerHTML = `
Your total: ${total}
` + renderTable(mine.slice().reverse(), “”); } // ====== INIT & EVENTS ====== (function init(){ const t = todayParts(); dateEl.textContent = `${t.dayName}, ${t.dateStr} • Year ${t.year}`; loadPrefs(); setCount(0); // Target pills document.querySelectorAll(‘#japa-app .pill’).forEach(p=>{ p.addEventListener(‘click’, ()=>{ document.querySelectorAll(‘#japa-app .pill’).forEach(x=>x.classList.remove(‘active’)); p.classList.add(‘active’); const v = p.dataset.target; customTargetEl.style.display = (v===”custom”) ? “inline-block” : “none”; targetDisplayEl.textContent = getTarget(); savePrefs(); }); }); customTargetEl.addEventListener(‘input’, ()=>{ targetDisplayEl.textContent = getTarget(); savePrefs(); }); // Counter buttons $(“btn-tap”).addEventListener(‘click’, ()=> setCount(count+1)); $(“btn-inc”).addEventListener(‘click’, ()=> setCount(count+1)); $(“btn-dec”).addEventListener(‘click’, ()=> setCount(count-1)); $(“btn-reset”).addEventListener(‘click’, ()=> setCount(0)); // Prefs nameEl.addEventListener(‘change’, savePrefs); emailEl.addEventListener(‘change’, ()=>{ savePrefs(); refreshViews(); }); mantraEl.addEventListener(‘change’, savePrefs); // Log $(“btn-log”).addEventListener(‘click’, async ()=>{ const name = nameEl.value.trim() || “Anonymous”; const email = (emailEl.value||””).trim().toLowerCase(); if(!email){ toast(“Please enter your email to log (subscribers only).”, false); return; } const mantra = mantraEl.value.trim() || “—”; const target = getTarget(); const t = todayParts(); if(count { tab.addEventListener(‘click’, ()=>{ document.querySelectorAll(‘#japa-app .tab’).forEach(x=>x.classList.remove(‘active’)); tab.classList.add(‘active’); const sel = tab.dataset.tab; document.querySelectorAll(‘#japa-app .tabpane’).forEach(p=>p.classList.add(‘hidden’)); $(“tab-“+sel).classList.remove(‘hidden’); }); }); refreshViews().catch(()=>{}); if(!GAS_ENDPOINT){ $(“today-table”).innerHTML = `
Local mode (per device). Connect Google Sheet for shared totals.
`; $(“alltime-table”).innerHTML = `
No shared data yet. Local entries only.
`; } })();