#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}
// ====== 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 `Date Name Mantra Count `;
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 `Rank Name Total 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 = `
🧘🏽♂️ Japa Counter (Subscribers of selfdiscovery.uk)
Your Name
Your Email (for your history)
Mantra
Target Count (per round)
27
54
108
Custom
−
Tap to Count Japa
/
+
Reset
Log Completed Round
Today
Leaderboard
My History
📅 Today’s Japa
Loading…
${emptyText}
`;
const head = `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 = `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.
`;
}
})();
