gouttes-yeux / index.html
ArthurZ's picture
ArthurZ HF Staff
Upload index.html with huggingface_hub
82de106 verified
Raw History Blame Contribute Delete
23.6 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#0b1220">
<title>Gouttes PKR 💧</title>
<style>
:root{
--bg:#0b1220; --card:#141d2e; --card2:#1b2740; --line:#26334d;
--txt:#e8eefc; --muted:#8da0c4; --accent:#4f8cff; --ok:#39d98a;
--warn:#ffb020; --danger:#ff5d6c; --chip:#22304d;
--shadow:0 6px 22px rgba(0,0,0,.35);
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background:var(--bg);color:var(--txt);padding:14px 12px 90px;max-width:560px;margin:0 auto}
h1{font-size:20px;margin:0 0 2px;display:flex;align-items:center;gap:8px}
.sub{color:var(--muted);font-size:13px;margin-bottom:14px}
.clock{font-variant-numeric:tabular-nums;font-weight:700}
.topbar{display:flex;justify-content:space-between;align-items:center;gap:10px;
background:var(--card);border:1px solid var(--line);border-radius:16px;padding:12px 14px;margin-bottom:12px}
.topbar .now{font-size:30px;font-weight:800;letter-spacing:.5px}
.topbar .day{font-size:12px;color:var(--muted);text-align:right}
.modebar{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}
.modebar button{flex:1;min-width:110px;background:var(--card);color:var(--muted);border:1px solid var(--line);padding:10px;font-size:13px;border-radius:12px}
.modebar button.active{background:rgba(79,140,255,.2);color:#a9c6ff;border-color:var(--accent)}
.modebar .notifbtn{flex:0 0 auto;min-width:0}
.modebar .notifbtn.on{background:rgba(57,217,138,.2);color:#7af0b6;border-color:var(--ok)}
.lasiknote{display:none;background:rgba(255,176,32,.1);border:1px solid rgba(255,176,32,.35);color:#ffd27a;
font-size:12px;border-radius:12px;padding:10px 12px;margin-bottom:12px;line-height:1.45}
.lasiknote.show{display:block}
.banner{border-radius:14px;padding:12px 14px;margin-bottom:12px;font-weight:600;font-size:14px;
display:none;align-items:center;gap:10px;border:1px solid}
.banner.wait{display:flex;background:rgba(255,176,32,.12);border-color:rgba(255,176,32,.4);color:#ffd27a}
.banner.go{display:flex;background:rgba(57,217,138,.12);border-color:rgba(57,217,138,.4);color:#7af0b6}
.next{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:12px 14px;margin-bottom:16px}
.next h2{font-size:13px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin:0 0 8px}
.next .row{display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-top:1px solid var(--line);font-size:14px}
.next .row:first-of-type{border-top:none}
.next .dot{width:10px;height:10px;border-radius:50%;display:inline-block;margin-right:8px}
.next .t{font-weight:700;font-variant-numeric:tabular-nums}
.next .late{color:var(--danger)}
.next .soon{color:var(--warn)}
.next .ok{color:var(--muted)}
.next .empty{color:var(--muted);font-size:13px;padding:4px 0}
.card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:14px;margin-bottom:14px;box-shadow:var(--shadow);border-left:5px solid var(--c)}
.card .head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
.card .name{font-size:17px;font-weight:800;display:flex;align-items:center;gap:8px}
.card .desc{font-size:12.5px;color:var(--muted);margin-top:2px;line-height:1.4}
.pill{font-size:11px;font-weight:700;padding:4px 9px;border-radius:999px;background:var(--chip);color:var(--txt);white-space:nowrap}
.pill.warnpill{background:rgba(255,176,32,.18);color:#ffd27a}
.progress{display:flex;align-items:center;gap:8px;margin:12px 0 4px;font-size:13px;color:var(--muted)}
.progress b{color:var(--txt)}
.slots{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0 12px}
.slot{font-size:12px;font-variant-numeric:tabular-nums;padding:5px 9px;border-radius:9px;background:var(--card2);color:var(--muted);border:1px solid var(--line)}
.slot.done{background:rgba(57,217,138,.16);color:#7af0b6;border-color:rgba(57,217,138,.4)}
.slot.next{background:rgba(79,140,255,.18);color:#a9c6ff;border-color:rgba(79,140,255,.5);font-weight:700}
.slot.late{background:rgba(255,93,108,.16);color:#ff9aa4;border-color:rgba(255,93,108,.45);font-weight:700}
.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
button{font-family:inherit;cursor:pointer;border:none;border-radius:12px;font-weight:700}
.give{flex:1;min-width:150px;background:var(--accent);color:#fff;padding:13px;font-size:15px}
.give:active{transform:scale(.98)}
.give.go{background:var(--ok)}
.give.blocked{background:transparent;color:var(--danger);border:1.5px dashed var(--danger);cursor:not-allowed;opacity:.85}
.give.notyet{background:var(--card2);color:var(--muted);border:1px solid var(--line)}
.mini{background:var(--card2);color:var(--txt);border:1px solid var(--line);padding:11px 12px;font-size:13px}
.mini.undo{color:#ff9aa4}
input[type=time]{background:var(--card2);color:var(--txt);border:1px solid var(--line);border-radius:10px;padding:9px;font-family:inherit;font-size:13px}
.phase{display:flex;gap:6px;flex-wrap:wrap;margin:6px 0 10px}
.phase button{font-size:11.5px;padding:6px 9px;background:var(--card2);color:var(--muted);border:1px solid var(--line)}
.phase button.active{background:rgba(79,140,255,.2);color:#a9c6ff;border-color:var(--accent)}
.footer{margin-top:18px;text-align:center}
.footer button{background:transparent;color:var(--muted);border:1px solid var(--line);padding:9px 14px;font-size:12px;margin:3px}
.note{font-size:11px;color:var(--muted);text-align:center;margin-top:14px;line-height:1.5}
details{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:10px 14px;margin-bottom:14px}
summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--muted)}
details ol{font-size:12.5px;color:var(--muted);line-height:1.6;padding-left:18px;margin:8px 0 0}
.howto{background:var(--card2);border:1px solid var(--line);border-radius:10px;padding:7px 11px;margin:2px 0 10px}
.howto summary{font-size:12px;color:var(--muted)}
.howto ol{font-size:12px;color:var(--muted);line-height:1.55;padding-left:16px;margin:7px 0 2px}
.howto li{margin-bottom:3px}
</style>
</head>
<body>
<h1>💧 Mes gouttes — yeux</h1>
<div class="sub">Clique « Donné » dès que tu mets une goutte → les prochaines heures se calculent toutes seules.</div>
<div class="topbar">
<div>
<div class="now clock" id="clock">--:--</div>
<div class="day" id="dayinfo"></div>
</div>
<div class="day" id="startbox"></div>
</div>
<div class="modebar">
<button id="mode_pkr" onclick="setMode('pkr')">PKR · mon traitement</button>
<button id="mode_lasik" onclick="setMode('lasik')">LASIK · type</button>
<button id="notifbtn" class="notifbtn" onclick="toggleNotif()">🔔 Rappels</button>
</div>
<div class="lasiknote" id="lasiknote"></div>
<div class="banner" id="banner"></div>
<details>
<summary>📋 Comment espacer (lis-moi)</summary>
<ol>
<li><b>5 min minimum entre deux produits différents</b> sinon la 1ère goutte fait déborder la 2ème.</li>
<li>1 seule goutte suffit (l'œil ne contient pas plus).</li>
<li>Ordre conseillé : antibiotique → corticoïde fort → cicatrisant/lubrifiant → rinçage en dernier. (Suis l'ordre de ton ophtalmo.)</li>
<li>Dacudose se garde au frais ❄️.</li>
</ol>
</details>
<div class="next">
<h2>⏭️ Prochaines doses</h2>
<div id="nextlist"></div>
</div>
<div id="cards"></div>
<div class="footer">
<button onclick="resetDay()">↺ Réinitialiser la journée</button>
<button onclick="resetAll()">🗑️ Tout effacer</button>
</div>
<div class="note">
Outil personnel d'aide-mémoire, sans valeur médicale. En cas de douleur intense, baisse de vision ou doute → ophtalmo.
</div>
<script>
/* ---------- Données des collyres ---------- */
const PKR_MEDS = [
{
id:'maxidrol', name:'Maxidrol', emoji:'🔴', color:'#ff5d6c',
desc:'Tobramycine + corticoïde — TRÈS fort. À donner en premier.',
taper:[
{label:'4×/j (début)', freq:4, intervalH:4},
{label:'3×/j · 7 jours', freq:3, intervalH:5},
{label:'2×/j · 7 jours', freq:2, intervalH:7},
{label:'1×/j · 7 jours', freq:1, intervalH:24},
],
howto:[
'Agiter le flacon avant usage 🫨',
'Se laver les mains',
'Ne pas toucher l\'œil ni les cils avec l\'embout',
'Regarder vers le haut, tirer la paupière du bas, déposer 1 goutte dans le cul-de-sac conjonctival inférieur',
'Fermer l\'œil et appuyer 2 min sur le coin interne (près du nez) 👃',
],
},
{ id:'quinofree', name:'Quinofree', emoji:'🟡', color:'#ffb020',
desc:'Ofloxacine (antibiotique) — 1 goutte, 4×/j pendant 7 jours.',
freq:4, intervalH:4, durationDays:7,
howto:[
'Se laver les mains',
'Ne pas toucher l\'œil avec l\'embout',
'Regarder en haut, tirer la paupière du bas, 1 goutte dans le cul-de-sac inférieur',
'Fermer l\'œil doucement quelques secondes',
] },
{ id:'nebraya', name:'Nebraya', emoji:'🟢', color:'#39d98a',
desc:'Cicatrisant — 1 goutte, 6×/j pendant 30 jours. + à volonté si douleur.',
freq:6, intervalH:2.5, durationDays:30, asNeeded:true,
howto:[
'Se laver les mains',
'1 goutte dans le cul-de-sac inférieur (paupière du bas)',
'Une goutte de plus possible si douleur',
] },
{ id:'vitadrop', name:'Vitadrop', emoji:'🔵', color:'#4f8cff',
desc:'Vitamine (solution ophtalmique) — 1 goutte, 4×/j pendant 15 jours.',
freq:4, intervalH:4, durationDays:15,
howto:[
'Se laver les mains',
'Ne pas toucher l\'œil avec l\'embout',
'1 goutte dans le cul-de-sac inférieur',
'Fermer l\'œil doucement',
] },
{ id:'dacudose', name:'Dacudose', emoji:'❄️', color:'#9aa6c4',
desc:'Acide borique (rinçage) — à volonté. Garder au frais.',
unlimited:true, intervalH:2,
howto:[
'Se laver les mains',
'Garder au frais ❄️',
'Rincer l\'œil, puis jeter l\'unidose après usage',
] },
];
/* Protocole LASIK — TYPE / GÉNÉRIQUE (cicatrisation plus rapide qu'en PKR).
Les marques et durées exactes varient selon le chirurgien : à adapter à ton ordonnance. */
const LASIK_MEDS = [
{ id:'lasik_antibio', name:'Antibiotique', emoji:'🟡', color:'#ffb020',
desc:'Ofloxacine / moxifloxacine — 1 goutte, 4×/j pendant ~7 jours.',
freq:4, intervalH:4, durationDays:7,
howto:[
'Se laver les mains',
'Ne pas toucher l\'œil avec l\'embout',
'Regarder en haut, tirer la paupière du bas, 1 goutte dans le cul-de-sac inférieur',
'Appuyer 1–2 min sur le coin interne (près du nez), fermer l\'œil',
] },
{ id:'lasik_cortico', name:'Corticoïde', emoji:'🔴', color:'#ff5d6c',
desc:'Dexaméthasone (anti-inflammatoire) — 1 goutte, 4×/j pendant ~7 jours (parfois toutes les 2h le jour J).',
freq:4, intervalH:4, durationDays:7,
howto:[
'Agiter si suspension 🫨',
'Se laver les mains',
'Ne pas toucher l\'œil avec l\'embout',
'1 goutte dans le cul-de-sac inférieur',
'Appuyer 1–2 min sur le coin interne (près du nez), fermer l\'œil',
] },
{ id:'lasik_larmes', name:'Larmes artificielles', emoji:'🟢', color:'#39d98a',
desc:'Sans conservateur — généreusement, jusqu\'à 1×/h la 1ère semaine, puis ≥4×/j pendant ~1 mois. À volonté.',
freq:6, intervalH:2, durationDays:30, asNeeded:true,
howto:[
'Se laver les mains',
'1 goutte dans le cul-de-sac inférieur',
'À renouveler dès que l\'œil est sec ou irrité',
] },
];
let MEDS = PKR_MEDS;
/* ---------- État ---------- */
const KEY='pkr_state_v2';
function todayKey(){const d=new Date();return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}
function loadState(){
let s=null; try{s=JSON.parse(localStorage.getItem(KEY));}catch(e){}
if(!s){ s={startDate:todayKey(), maxidrolPhase:0, logs:{}, lastDrop:null, mode:'pkr', notif:false}; }
if(!s.logs) s.logs={};
if(!s.mode) s.mode='pkr';
// pré-remplissage : Maxidrol déjà donné à 12h10 aujourd'hui
const tk=todayKey();
if(!s.logs[tk]){ s.logs[tk]={}; if(s.startDate===tk){ s.logs[tk].maxidrol=['12:10']; } }
return s;
}
let S=loadState();
MEDS = (S.mode==='lasik') ? LASIK_MEDS : PKR_MEDS;
function save(){localStorage.setItem(KEY,JSON.stringify(S));}
function setMode(m){ S.mode=m; MEDS=(m==='lasik')?LASIK_MEDS:PKR_MEDS; firedNotif={}; save(); render(); }
function today(){const tk=todayKey(); if(!S.logs[tk])S.logs[tk]={}; return S.logs[tk];}
/* ---------- Helpers temps ---------- */
function nowStr(){const d=new Date();return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');}
function hm(str){const[a,b]=str.split(':').map(Number);return a*60+b;}
function fromMin(m){m=((m%1440)+1440)%1440;return String(Math.floor(m/60)).padStart(2,'0')+':'+String(m%60).padStart(2,'0');}
function nowMin(){const d=new Date();return d.getHours()*60+d.getMinutes();}
function daysSinceStart(){
const a=new Date(S.startDate+'T00:00:00'), b=new Date(todayKey()+'T00:00:00');
return Math.round((b-a)/86400000);
}
/* ---------- Logique d'un collyre ---------- */
function medCfg(m){
if(m.taper) return {...m, ...m.taper[S.maxidrolPhase]};
return m;
}
function give(id,time){
const t=today(); if(!t[id])t[id]=[];
t[id].push(time||nowStr());
t[id].sort((a,b)=>hm(a)-hm(b));
if(!time){ S.lastDrop={id, ts:Date.now()}; }
save(); render();
}
function undo(id){ const t=today(); if(t[id]&&t[id].length){t[id].pop(); save(); render();} }
/* planned times: étalées de la 1ère goutte du jour → minuit max */
function planned(cfg){
const t=today(); const given=t[cfg.id]||[];
if(cfg.unlimited||!cfg.freq) return null;
const anchor = given.length? hm(given[0]) : nowMin();
const END = 1440; // minuit
if(cfg.freq===1) return [anchor];
const step = Math.max(0,(END-anchor))/(cfg.freq-1);
const arr=[];
for(let k=0;k<cfg.freq;k++) arr.push(Math.round(anchor + k*step));
return arr;
}
/* ---------- Calcul par collyre (partagé render + tick) ---------- */
function blockState(){
if(!S.lastDrop) return {blocked:false,elapsed:1e9};
const elapsed=Math.floor((Date.now()-S.lastDrop.ts)/1000);
return {blocked:elapsed<300, elapsed, rem:300-elapsed, fromId:S.lastDrop.id};
}
function computeMed(m){
const cfg=medCfg(m);
const given=(today()[m.id]||[]);
const plan=planned(cfg);
const nm=nowMin();
let nextMin=null,status='';
if(plan){
const idx=given.length;
if(idx<plan.length){ nextMin=plan[idx]; status=(nm>=nextMin)?'late':(nextMin-nm<=30?'soon':'ok'); }
}
// slots
let slotsHtml='';
if(plan){
plan.forEach((p,i)=>{
let cls='slot';
if(i<given.length) cls+=' done';
else if(i===given.length) cls+=(nm>=p?' late':' next');
slotsHtml+='<span class="'+cls+'">'+(i<given.length?'✓ '+given[i]:fromMin(p))+'</span>';
});
} else if(m.unlimited){
given.forEach(g=>slotsHtml+='<span class="slot done">✓ '+g+'</span>');
if(!given.length) slotsHtml='<span class="slot">à volonté</span>';
}
// progress
let progress='';
if(cfg.freq){ progress='Aujourd\'hui : <b>'+given.length+' / '+cfg.freq+'</b>'+(nextMin!=null?' · prochaine <b>'+fromMin(nextMin)+'</b>':' ✅ terminé'); }
else if(m.unlimited){ progress='<b>'+given.length+'</b> aujourd\'hui'; }
return {cfg,given,plan,nextMin,status,slotsHtml,progress,goNow:(nextMin!=null&&nm>=nextMin)};
}
/* ---------- Rendu ---------- */
function render(){
document.getElementById('clock').textContent=nowStr();
document.getElementById('dayinfo').textContent='Jour '+(daysSinceStart()+1)+' · '+todayKey().split('-').reverse().join('/');
document.getElementById('startbox').innerHTML='Début traitement<br><input type="date" value="'+S.startDate+'" onchange="S.startDate=this.value;save();render()" style="background:var(--card2);color:var(--txt);border:1px solid var(--line);border-radius:8px;padding:4px;font-size:11px;margin-top:3px">';
/* mode + notif */
document.getElementById('mode_pkr').className = (S.mode==='pkr'?'active':'');
document.getElementById('mode_lasik').className = (S.mode==='lasik'?'active':'');
const ln=document.getElementById('lasiknote');
if(S.mode==='lasik'){ ln.className='lasiknote show'; ln.innerHTML='ℹ️ Protocole LASIK <b>générique/type</b> (cicatrisation plus rapide). Marques et durées varient selon le chirurgien — adapte à ton ordonnance.'; }
else ln.className='lasiknote';
const nb=document.getElementById('notifbtn');
const granted = ('Notification' in window) && Notification.permission==='granted' && S.notif;
nb.className = 'notifbtn'+(granted?' on':''); nb.textContent = granted?'🔔 Rappels ON':'🔕 Rappels';
/* cartes */
const cardsEl=document.getElementById('cards'); cardsEl.innerHTML='';
MEDS.forEach(m=>{
const d=computeMed(m);
/* pill */
let pill='';
if(m.durationDays!==undefined){
const rem=m.durationDays-daysSinceStart();
pill='<span class="pill">'+(rem>0?'reste '+rem+' j':'fini ?')+'</span>';
} else if(m.unlimited){ pill='<span class="pill warnpill">à volonté ❄️</span>'; }
/* phases maxidrol */
let phaseHtml='';
if(m.taper){
phaseHtml='<div class="phase">';
m.taper.forEach((p,i)=>{ phaseHtml+='<button class="'+(i===S.maxidrolPhase?'active':'')+'" onclick="S.maxidrolPhase='+i+';save();render()">'+p.label+'</button>'; });
phaseHtml+='</div>';
}
/* comment l'utiliser */
let howtoHtml='';
if(m.howto){
howtoHtml='<details class="howto"><summary>💧 Comment l\'utiliser</summary><ol>'
+m.howto.map(s=>'<li>'+s+'</li>').join('')+'</ol></details>';
}
const card=document.createElement('div');
card.className='card'; card.style.setProperty('--c',m.color);
card.innerHTML=
'<div class="head"><div><div class="name">'+m.emoji+' '+m.name+'</div><div class="desc">'+m.desc+'</div></div>'+pill+'</div>'
+phaseHtml
+'<div class="progress" id="prog_'+m.id+'">'+d.progress+'</div>'
+howtoHtml
+'<div class="slots" id="slots_'+m.id+'">'+d.slotsHtml+'</div>'
+'<div class="actions">'
+'<button class="give" id="give_'+m.id+'" onclick="give(\''+m.id+'\')"></button>'
+'<input type="time" id="t_'+m.id+'" value="'+nowStr()+'">'
+'<button class="mini" onclick="give(\''+m.id+'\',document.getElementById(\'t_'+m.id+'\').value)">+ heure</button>'
+'<button class="mini undo" onclick="undo(\''+m.id+'\')">↶</button>'
+'</div>';
cardsEl.appendChild(card);
});
tick(); // remplit boutons, banner, liste, compteur
}
/* ---------- Ticker à la seconde (sans casser les inputs) ---------- */
function tick(){
document.getElementById('clock').textContent=nowStr();
const b=blockState();
const banner=document.getElementById('banner'); banner.className='banner';
if(b.blocked){
const mm=Math.floor(b.rem/60), ss=b.rem%60;
banner.className='banner wait';
banner.innerHTML='⏳ Bloqué encore <b style="margin:0 4px">'+mm+':'+String(ss).padStart(2,'0')+'</b> — 5 min entre 2 produits.';
} else if(b.elapsed<900){
banner.className='banner go';
banner.innerHTML='✅ OK pour le produit suivant.';
}
const upcoming=[];
MEDS.forEach(m=>{
const d=computeMed(m);
const sl=document.getElementById('slots_'+m.id); if(sl) sl.innerHTML=d.slotsHtml;
const pg=document.getElementById('prog_'+m.id); if(pg) pg.innerHTML=d.progress;
const btn=document.getElementById('give_'+m.id);
if(btn){
if(b.blocked){
btn.disabled=true; btn.className='give blocked'; btn.style.opacity=1;
const mm=Math.floor(b.rem/60), ss=b.rem%60;
btn.textContent='⛔ Attends '+mm+':'+String(ss).padStart(2,'0');
} else {
btn.disabled=false; btn.style.opacity=1;
if(d.cfg.unlimited){
btn.className='give'; btn.textContent='✓ Donné maintenant';
} else if(d.goNow || d.status==='soon'){
btn.className='give'+(d.goNow?' go':''); btn.textContent='✓ Donné'+(d.goNow?' (maintenant !)':' maintenant');
} else if(d.nextMin!=null){
btn.className='give notyet'; btn.textContent='⏰ Pas l\'heure · '+fromMin(d.nextMin);
} else {
btn.className='give notyet'; btn.textContent='✓ Fini aujourd\'hui';
}
}
}
// rappel "tu peux donner X" (une fois par créneau, quand c'est l'heure et non bloqué)
if(S.notif && d.nextMin!=null && d.goNow && !b.blocked){
const k=S.mode+':'+m.id+':'+d.given.length+':'+todayKey();
if(!firedNotif[k]){ firedNotif[k]=1; notify('💧 '+m.name,'Tu peux donner '+m.name+' maintenant.'); }
}
if(d.nextMin!=null) upcoming.push({m,min:d.nextMin,status:d.status});
});
// rappel fin d'espacement 5 min
if(S.notif && wasBlocked && !b.blocked){ notify('✅ Espacement fini','Tu peux donner la goutte suivante (5 min écoulées).'); }
wasBlocked=b.blocked;
upcoming.sort((a,b)=>a.min-b.min);
const nl=document.getElementById('nextlist');
if(!upcoming.length){ nl.innerHTML='<div class="empty">Tout est donné pour les créneaux planifiés 🎉</div>'; }
else{
nl.innerHTML=upcoming.map(u=>{
const cls=u.status==='late'?'late':u.status==='soon'?'soon':'ok';
const txt=u.status==='late'?'à donner !':u.status==='soon'?'bientôt':'';
return '<div class="row"><div><span class="dot" style="background:'+u.m.color+'"></span>'+u.m.emoji+' '+u.m.name+'</div>'
+'<div class="t '+cls+'">'+fromMin(u.min)+(txt?' · '+txt:'')+'</div></div>';
}).join('');
}
}
/* ---------- Notifications ---------- */
let firedNotif={}; // clés déjà notifiées (en mémoire, par jour)
let wasBlocked=false;
function toggleNotif(){
if(!('Notification' in window)){ alert('Les notifications ne sont pas supportées par ce navigateur.'); return; }
if(S.notif && Notification.permission==='granted'){ S.notif=false; save(); render(); return; }
Notification.requestPermission().then(p=>{
S.notif=(p==='granted'); save(); render();
if(p==='granted') notify('🔔 Rappels activés','Tu seras prévenu quand une goutte est à donner.');
else alert('Autorise les notifications dans les réglages du navigateur pour activer les rappels.');
});
}
function notify(title,body){
try{
if(('Notification' in window) && Notification.permission==='granted'){ new Notification(title,{body,tag:title}); }
if(navigator.vibrate) navigator.vibrate(200);
}catch(e){}
}
function resetDay(){ if(confirm('Effacer les gouttes données aujourd\'hui ?')){ S.logs[todayKey()]={}; S.lastDrop=null; firedNotif={}; save(); render(); } }
function resetAll(){ if(confirm('Tout remettre à zéro (historique + date de début) ?')){ localStorage.removeItem(KEY); S=loadState(); save(); render(); } }
render();
setInterval(tick,1000); // compteur 5 min + horloge + statuts (sans casser les champs heure)
// réveille l'horloge quand on revient sur la page (les navigateurs gèlent les timers en arrière-plan)
document.addEventListener('visibilitychange',()=>{ if(!document.hidden) tick(); });
window.addEventListener('focus',tick);
window.addEventListener('pageshow',tick);
</script>
</body>
</html>