Repository navigation
Expand file tree
/
Copy pathapp.js
More file actions
13 lines (13 loc) · 7.83 KB
/
Copy pathapp.js
File metadata and controls
13 lines (13 loc) · 7.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
const P=window.PROJECT;const lang=document.documentElement.lang==='en'?'en':'nl';const c=P.copy[lang];const $=s=>document.querySelector(s);const esc=s=>String(s??'').replace(/[&<>"']/g,x=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[x]));const common={nl:{saved:'Bewaard op dit apparaat',remove:'Verwijderen',done:'Klaar',open:'Open',today:'vandaag',count:'items',all:'Alles wissen',confirm:'Alles wissen? Dit verwijdert de opgeslagen gegevens.',summary:'Overzicht',error:'Opslag niet beschikbaar. Pas de privacy-instellingen van je browser aan en probeer opnieuw.',flip:'Volgende'},en:{saved:'Saved on this device',remove:'Remove',done:'Done',open:'Open',today:'today',count:'items',all:'Clear all',confirm:'Clear everything? This deletes saved data.',summary:'Summary',error:'Storage is unavailable. Check your browser privacy settings and try again.',flip:'Next'}}[lang];
const key='local-'+P.slug;let state;try{state=JSON.parse(localStorage.getItem(key)||'null')}catch{state=null}if(!state||typeof state!=='object')state={items:[],habits:[],logs:[]};const save=()=>{try{localStorage.setItem(key,JSON.stringify(state));$('#saveNote').textContent=common.saved}catch{$('#saveNote').textContent=common.error}};const today=()=>new Date().toLocaleDateString('en-CA');const dateVal=d=>d?new Date(d+'T12:00:00').toLocaleDateString(lang==='nl'?'nl-BE':'en-GB',{day:'numeric',month:'short',year:'numeric'}):'';
document.title=P.name+' — '+c.title;document.documentElement.style.setProperty('--accent',P.theme);document.body.innerHTML='<header><a class="brand" href="../index.html"><span class="mark">'+esc(P.symbol)+'</span>'+esc(P.name)+'</a><a class="lang" href="'+(lang==='nl'?'en/index.html':'../index.html')+'">'+(lang==='nl'?'English ↗':'Nederlands ↗')+'</a></header><main><section class="hero"><p class="eyebrow">'+esc(c.tag)+'</p><h1>'+esc(c.title)+'</h1><p class="intro">'+esc(c.intro)+'</p></section><section class="workspace"><div class="main-card"><div class="section-head"><div><p class="eyebrow">'+esc(common.summary)+'</p><h2>'+esc(c.metric)+'</h2></div><strong class="total" id="total">0</strong></div><div id="content"></div><p class="save-note" id="saveNote">'+esc(common.saved)+'</p></div><aside class="form-card"><p class="eyebrow">'+esc(P.name.toUpperCase())+'</p><h2>'+esc(c.add)+'</h2><form id="entryForm"></form></aside></section><p class="privacy">'+(lang==='nl'?'🔒 Je gegevens blijven lokaal in deze browser. Ze worden niet verstuurd.':'🔒 Your data stays local in this browser. Nothing is sent.')+'</p></main><footer>'+esc(P.name)+' <span>· '+esc(common.saved)+'</span></footer>';
const fieldHtml=f=>'<label for="f-'+esc(f[0])+'">'+esc(f[1])+'</label>'+ (f[2]==='textarea'?'<textarea id="f-'+esc(f[0])+'" name="'+esc(f[0])+'" rows="5" placeholder="'+esc(f[3])+'" required></textarea>':'<input id="f-'+esc(f[0])+'" name="'+esc(f[0])+'" type="'+esc(f[2])+'" '+(f[2]==='number'?'min="0" step="any"':'')+' placeholder="'+esc(f[3])+'" '+(f[2]==='date'?'':'required')+'>');
const form=$('#entryForm');form.innerHTML=c.fields.map(fieldHtml).join('')+'<button class="primary" type="submit">'+esc(c.add)+' <span aria-hidden="true">→</span></button>';
const delBtn=(arr,i)=>'<button class="remove" data-remove="'+i+'" type="button">'+esc(common.remove)+'</button>';
function renderLog(){const rows=state.items;$('#total').textContent=P.metric==='pages'?rows.reduce((a,x)=>a+(+x.pages||0),0).toLocaleString(lang==='nl'?'nl-BE':'en-US'):rows.length.toLocaleString(lang==='nl'?'nl-BE':'en-US');if(!rows.length){$('#content').innerHTML='<p class="empty">'+esc(c.empty)+'</p>';return}$('#content').innerHTML='<ul class="items">'+rows.slice().reverse().map((r,i)=>'<li><div><strong>'+esc(r.title||r.item||r.person||r.pet||r.plant||r.gift||c.metric)+'</strong><p>'+c.fields.filter(f=>f[0]!=='title'&&f[0]!=='item'&&r[f[0]]).map(f=>esc(f[1])+': '+esc(f[2]==='date'?dateVal(r[f[0]]):r[f[0]])).join(' · ')+'</p></div>'+delBtn(rows.length-1-i)+'</li>').join('')+'</ul>'}
function renderChecklist(){const rows=state.items;const done=rows.filter(x=>x.done).length;$('#total').textContent=done+' / '+rows.length;$('#content').innerHTML=rows.length?'<ul class="items">'+rows.map((r,i)=>'<li class="check-row '+(r.done?'is-done':'')+'"><label><input type="checkbox" data-check="'+i+'" '+(r.done?'checked':'')+'><span><strong>'+esc(r.title)+'</strong><small>'+c.fields.filter(f=>f[0]!=='title'&&r[f[0]]).map(f=>esc(f[1])+': '+esc(f[2]==='date'?dateVal(r[f[0]]):r[f[0]])).join(' · ')+'</small></span></label>'+delBtn(i)+'</li>').join('')+'</ul>':'<p class="empty">'+esc(c.empty)+'</p>'}
function renderHabit(){const habits=state.habits;const done=habits.filter(h=>h.days.includes(today())).length;$('#total').textContent=done+' / '+habits.length;$('#content').innerHTML=habits.length?'<ul class="items">'+habits.map((h,i)=>{const checked=h.days.includes(today());return '<li class="check-row '+(checked?'is-done':'')+'"><label><input type="checkbox" data-habit="'+i+'" '+(checked?'checked':'')+'><span><strong>'+esc(h.title)+'</strong><small>'+h.days.length+' '+(lang==='nl'?'dagen afgevinkt':'days checked')+'</small></span></label>'+delBtn(i)+'</li>'}).join('')+'</ul>':'<p class="empty">'+esc(c.empty)+'</p>'}
function renderCalc(){if(P.kind==='tip'){const bill=+form.elements.bill.value||0,tip=+form.elements.tip.value||0,people=Math.max(1,+form.elements.people.value||1),total=bill*(1+tip/100);$('#total').textContent=new Intl.NumberFormat(lang==='nl'?'nl-BE':'en-US',{style:'currency',currency:lang==='nl'?'EUR':'USD'}).format(total/people);$('#content').innerHTML='<div class="result"><div><span>'+esc(lang==='nl'?'Rekening':'Bill')+'</span><strong>'+new Intl.NumberFormat(lang==='nl'?'nl-BE':'en-US',{style:'currency',currency:lang==='nl'?'EUR':'USD'}).format(bill)+'</strong></div><div><span>'+esc(lang==='nl'?'Fooi':'Tip')+' ('+tip+'%)</span><strong>'+new Intl.NumberFormat(lang==='nl'?'nl-BE':'en-US',{style:'currency',currency:lang==='nl'?'EUR':'USD'}).format(bill*tip/100)+'</strong></div><div><span>'+esc(lang==='nl'?'Totaal':'Total')+'</span><strong>'+new Intl.NumberFormat(lang==='nl'?'nl-BE':'en-US',{style:'currency',currency:lang==='nl'?'EUR':'USD'}).format(total)+'</strong></div></div>'}else{$('#total').textContent='× '+((+form.elements.target.value||0)/Math.max(1,+form.elements.servings.value||1)).toFixed(2);const ratio=(+form.elements.target.value||0)/Math.max(1,+form.elements.servings.value||1);const lines=(form.elements.ingredients.value||'').split(/\r?\n/).filter(Boolean).map(line=>line.replace(/^(\s*)(\d+(?:[.,]\d+)?)/,(all,space,num)=>space+(parseFloat(num.replace(',','.'))*ratio).toLocaleString(lang==='nl'?'nl-BE':'en-GB',{maximumFractionDigits:2})));$('#content').innerHTML=lines.length?'<ul class="items">'+lines.map(x=>'<li>'+esc(x)+'</li>').join('')+'</ul>':'<p class="empty">'+esc(c.empty)+'</p>'}}
function render(){if(P.kind==='log')renderLog();else if(P.kind==='checklist')renderChecklist();else if(P.kind==='habit')renderHabit();else renderCalc()}
if(P.kind==='tip'||P.kind==='scale'){form.addEventListener('input',renderCalc);form.addEventListener('submit',e=>{e.preventDefault();renderCalc()})}else{form.addEventListener('submit',e=>{e.preventDefault();const data=Object.fromEntries(new FormData(form).entries());if(P.kind==='habit')state.habits.push({title:data.title,days:[]});else if(P.kind==='checklist')state.items.push({...data,done:false});else state.items.push(data);save();form.reset();render()})}
$('#content').addEventListener('change',e=>{if(e.target.matches('[data-check]')){state.items[+e.target.dataset.check].done=e.target.checked;save();render()}if(e.target.matches('[data-habit]')){const h=state.habits[+e.target.dataset.habit];h.days=h.days.filter(x=>x!==today());if(e.target.checked)h.days.push(today());save();render()}});$('#content').addEventListener('click',e=>{const b=e.target.closest('[data-remove]');if(!b)return;(P.kind==='habit'?state.habits:state.items).splice(+b.dataset.remove,1);save();render()});render();