sektaolga-fodmapcommits36fabba
Analyzing diff...
index.html+35 −4
index c5b203b..a7829d3 100644 @@ -4,7 +4,7 @@ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <title>FODMAP-конструктор приёма пищи</title> <style> -:root{--bg:#f6f3ed;--card:#fff;--ink:#282723;--muted:#77736a;--green:#5c9b6a;--amber:#d8a23d;--red:#cf615a;--line:#e6e0d7;--accent:#7569a8}*{box-sizing:border-box}body{margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Arial,sans-serif;background:var(--bg);color:var(--ink)}.wrap{max-width:1180px;margin:auto;padding:28px}.top{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:20px}h1{font-size:30px;margin:0 0 6px}.sub{color:var(--muted);max-width:760px}.grid{display:grid;grid-template-columns:1.35fr .65fr;gap:18px}.card{background:var(--card);border:1px solid var(--line);border-radius:20px;padding:20px;box-shadow:0 8px 30px #00000008}h2{font-size:18px;margin:0 0 14px}.add{display:grid;grid-template-columns:1fr 110px 105px 48px;gap:10px;margin-bottom:16px}select,input,textarea,button{font:inherit;border:1px solid var(--line);border-radius:12px;padding:11px 12px;background:white;color:var(--ink)}button{cursor:pointer;font-weight:650}.primary{background:var(--ink);color:white;border-color:var(--ink)}.meal-row{display:grid;grid-template-columns:1fr 105px 88px 38px;gap:9px;align-items:center;padding:10px 0;border-top:1px solid var(--line)}.tag{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:700}.dot{width:10px;height:10px;border-radius:50%}.g{background:var(--green)}.a{background:var(--amber)}.r{background:var(--red)}.u{background:#9a958c}.remove{border:0;background:transparent;font-size:20px;padding:4px}.meterbox{display:flex;gap:22px;align-items:center}.glass{width:105px;height:220px;border:4px solid #b9b3a9;border-top:0;border-radius:0 0 28px 28px;position:relative;overflow:hidden;background:#faf9f6}.fill{position:absolute;left:0;right:0;bottom:0;height:0%;background:var(--green);transition:.3s}.critical{position:absolute;left:0;right:0;bottom:75%;border-top:2px dashed #7e786e;z-index:2}.critical span{position:absolute;right:4px;top:-21px;font-size:10px;color:#6e685f}.score{font-size:42px;font-weight:800}.status{font-weight:750;margin-top:5px}.small{font-size:12px;color:var(--muted);line-height:1.45}.groups{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:15px}.group{background:#f7f5f1;padding:8px 10px;border-radius:10px;font-size:12px}.advice{margin-top:14px;padding:13px;border-radius:14px;background:#f6f4fb;line-height:1.5}.advice b{display:block;margin-bottom:4px}.adapt{display:grid;gap:8px;margin-top:10px}.adapt button{text-align:left;background:white}.journal{margin-top:18px}.symptoms{display:grid;grid-template-columns:1fr 1fr;gap:10px}.history{margin-top:12px}.entry{padding:14px 0;border-top:1px solid var(--line);font-size:13px}.typeahead{position:relative}.typeahead input{width:100%}.suggest{display:none;position:absolute;z-index:30;left:0;right:0;top:100%;margin-top:4px;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:0 10px 30px #0000001f;max-height:260px;overflow:auto}.suggest.open{display:block}.suggest div{padding:9px 12px;cursor:pointer;font-size:14px}.suggest div.sel,.suggest div:hover{background:#f2efe8}.suggest .hi{color:var(--muted);font-size:12px;margin-left:6px}.suggest .sechead{padding:7px 12px 4px;font-size:11px;font-weight:750;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);background:#faf9f6;cursor:default;position:sticky;top:0}.suggest .none{color:var(--muted);cursor:default}.dayfold{border:1px solid var(--line);border-radius:14px;margin-top:12px;background:#fff;overflow:hidden}.dayfold[open]{box-shadow:0 6px 20px #00000008}.dayfold>summary{list-style:none;cursor:pointer;padding:13px 15px;display:flex;flex-wrap:wrap;align-items:center;gap:10px;background:#faf9f6;font-size:14px}.dayfold>summary::-webkit-details-marker{display:none}.dayfold>summary:before{content:'▸';color:var(--muted);transition:.15s;display:inline-block}.dayfold[open]>summary:before{transform:rotate(90deg)}.dayfold>summary:hover{background:#f2efe8}.dayname{font-weight:750;text-transform:capitalize}.daychips{display:inline-flex;gap:6px}.chip{display:inline-flex;align-items:center;gap:4px;background:#fff;border:1px solid var(--line);border-radius:20px;padding:2px 8px;font-size:12px;font-weight:700}.daymeta{color:var(--muted);font-size:12px;margin-left:auto}.daygroups{display:flex;flex-wrap:wrap;gap:6px;padding:0 15px 4px}.gchip{background:#f6f4fb;color:var(--accent);border-radius:20px;padding:2px 9px;font-size:11px;font-weight:700}.daybody{padding:0 15px 8px}.daybody .entry:first-child{border-top:0}.daychart{margin:16px 0;padding:14px;border:1px solid var(--line);border-radius:14px;background:#faf9f6}.chart-controls{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}.chart-body{display:flex;gap:20px;flex-wrap:wrap;align-items:flex-start}.chart-legend{display:grid;gap:5px;font-size:12px;min-width:190px}.chart-legend div{display:flex;align-items:center;gap:7px;cursor:pointer;padding:3px 5px;border-radius:8px}.chart-legend div:hover{background:#efece5}.chart-legend i{width:11px;height:11px;border-radius:3px;display:inline-block;flex:none}.chart-bars{display:grid;gap:6px;font-size:12px;min-width:200px;flex:1}.bar-track{background:#ece8e0;border-radius:6px;height:12px;overflow:hidden}.bar-fill{height:100%;border-radius:6px}.chart-detail{margin-top:10px;line-height:1.5}.chart-slice{cursor:pointer}.chart-slice:hover{opacity:.82} +:root{--bg:#f6f3ed;--card:#fff;--ink:#282723;--muted:#77736a;--green:#5c9b6a;--amber:#d8a23d;--red:#cf615a;--line:#e6e0d7;--accent:#7569a8}*{box-sizing:border-box}body{margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Arial,sans-serif;background:var(--bg);color:var(--ink)}.wrap{max-width:1180px;margin:auto;padding:28px}.top{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:20px}h1{font-size:30px;margin:0 0 6px}.sub{color:var(--muted);max-width:760px}.grid{display:grid;grid-template-columns:1.35fr .65fr;gap:18px}.card{background:var(--card);border:1px solid var(--line);border-radius:20px;padding:20px;box-shadow:0 8px 30px #00000008}h2{font-size:18px;margin:0 0 14px}.add{display:grid;grid-template-columns:1fr 110px 105px 48px;gap:10px;margin-bottom:16px}select,input,textarea,button{font:inherit;border:1px solid var(--line);border-radius:12px;padding:11px 12px;background:white;color:var(--ink)}button{cursor:pointer;font-weight:650}.primary{background:var(--ink);color:white;border-color:var(--ink)}.meal-row{display:grid;grid-template-columns:1fr 105px 88px 38px;gap:9px;align-items:center;padding:10px 0;border-top:1px solid var(--line)}.tag{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:700}.dot{width:10px;height:10px;border-radius:50%}.g{background:var(--green)}.a{background:var(--amber)}.r{background:var(--red)}.u{background:#9a958c}.remove{border:0;background:transparent;font-size:20px;padding:4px}.meterbox{display:flex;gap:22px;align-items:center}.glass{width:105px;height:220px;border:4px solid #b9b3a9;border-top:0;border-radius:0 0 28px 28px;position:relative;overflow:hidden;background:#faf9f6}.fill{position:absolute;left:0;right:0;bottom:0;height:0%;background:var(--green);transition:.3s}.critical{position:absolute;left:0;right:0;bottom:75%;border-top:2px dashed #7e786e;z-index:2}.critical span{position:absolute;right:4px;top:-21px;font-size:10px;color:#6e685f}.score{font-size:42px;font-weight:800}.status{font-weight:750;margin-top:5px}.small{font-size:12px;color:var(--muted);line-height:1.45}.groups{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:15px}.group{background:#f7f5f1;padding:8px 10px;border-radius:10px;font-size:12px}.advice{margin-top:14px;padding:13px;border-radius:14px;background:#f6f4fb;line-height:1.5}.advice b{display:block;margin-bottom:4px}.adapt{display:grid;gap:8px;margin-top:10px}.adapt button{text-align:left;background:white}.journal{margin-top:18px}.symptoms{display:grid;grid-template-columns:1fr 1fr;gap:10px}.history{margin-top:12px}.entry{padding:14px 0;border-top:1px solid var(--line);font-size:13px}.typeahead{position:relative}.typeahead input{width:100%}.suggest{display:none;position:absolute;z-index:30;left:0;right:0;top:100%;margin-top:4px;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:0 10px 30px #0000001f;max-height:260px;overflow:auto}.suggest.open{display:block}.suggest div{padding:9px 12px;cursor:pointer;font-size:14px}.suggest div.sel,.suggest div:hover{background:#f2efe8}.suggest .hi{color:var(--muted);font-size:12px;margin-left:6px}.suggest .sechead{padding:7px 12px 4px;font-size:11px;font-weight:750;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);background:#faf9f6;cursor:default;position:sticky;top:0}.suggest .none{color:var(--muted);cursor:default}.dayfold{border:1px solid var(--line);border-radius:14px;margin-top:12px;background:#fff;overflow:hidden}.dayfold[open]{box-shadow:0 6px 20px #00000008}.dayfold>summary{list-style:none;cursor:pointer;padding:13px 15px;display:flex;flex-wrap:wrap;align-items:center;gap:10px;background:#faf9f6;font-size:14px}.dayfold>summary::-webkit-details-marker{display:none}.dayfold>summary:before{content:'▸';color:var(--muted);transition:.15s;display:inline-block}.dayfold[open]>summary:before{transform:rotate(90deg)}.dayfold>summary:hover{background:#f2efe8}.dayname{font-weight:750;text-transform:capitalize}.daychips{display:inline-flex;gap:6px}.chip{display:inline-flex;align-items:center;gap:4px;background:#fff;border:1px solid var(--line);border-radius:20px;padding:2px 8px;font-size:12px;font-weight:700}.daymeta{color:var(--muted);font-size:12px;margin-left:auto}.dayload{flex-basis:100%;display:flex;align-items:center;gap:9px;font-size:12px}.dayload-track{flex:1;height:8px;background:#ece8e0;border-radius:6px;overflow:hidden;min-width:80px}.dayload-fill{display:block;height:100%;border-radius:6px;transition:.3s}.dayload-num{font-weight:800;min-width:38px;text-align:right}.dayload-word{color:var(--muted);min-width:120px}.dayactions{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:8px 15px 4px}.dayexport{margin-left:auto;font-size:12px;padding:5px 11px;border-radius:10px;background:#fff}.dayexport:hover{background:#f2efe8}.daygroups{display:flex;flex-wrap:wrap;gap:6px;padding:0 15px 4px}.gchip{background:#f6f4fb;color:var(--accent);border-radius:20px;padding:2px 9px;font-size:11px;font-weight:700}.daybody{padding:0 15px 8px}.daybody .entry:first-child{border-top:0}.daychart{margin:16px 0;padding:14px;border:1px solid var(--line);border-radius:14px;background:#faf9f6}.chart-controls{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}.chart-body{display:flex;gap:20px;flex-wrap:wrap;align-items:flex-start}.chart-legend{display:grid;gap:5px;font-size:12px;min-width:190px}.chart-legend div{display:flex;align-items:center;gap:7px;cursor:pointer;padding:3px 5px;border-radius:8px}.chart-legend div:hover{background:#efece5}.chart-legend i{width:11px;height:11px;border-radius:3px;display:inline-block;flex:none}.chart-bars{display:grid;gap:6px;font-size:12px;min-width:200px;flex:1}.bar-track{background:#ece8e0;border-radius:6px;height:12px;overflow:hidden}.bar-fill{height:100%;border-radius:6px}.chart-detail{margin-top:10px;line-height:1.5}.chart-slice{cursor:pointer}.chart-slice:hover{opacity:.82} .entry-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start} .entry-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px} .entry-actions button{padding:7px 9px;font-size:12px} @@ -637,6 +637,36 @@ function saveReaction(){ function deleteReaction(mealId,reactionId){let entries=getEntries(),e=entries.find(x=>x.id===mealId);if(!e)return;e.reactions=(e.reactions||[]).filter(r=>r.id!==reactionId);setEntries(entries);showHistory();render()} let openDays=JSON.parse(sessionStorage.getItem('fodmapOpenDays')||'null'); function toggleDay(k,el){openDays=openDays||{};openDays[k]=el.open;sessionStorage.setItem('fodmapOpenDays',JSON.stringify(openDays))} +const foodByName=(()=>{let m=new Map();foods.forEach(f=>m.set(f.name.toLocaleLowerCase('ru'),f));return m})(); +function dayLoadPct(list){ + let total=0; + list.forEach(e=>(e.meal||[]).forEach(m=>{let f=foodByName.get((m.name||'').toLocaleLowerCase('ru'));if(f)total+=contribution(f,m.g||0)})); + return Math.round(total*100); +} +function loadColor(p){return p<60?'var(--green)':p<100?'var(--amber)':'var(--red)'} +function loadWord(p){return p<1?'нагрузки нет':p<60?'низкая нагрузка':p<100?'нагрузка растёт':'высокая нагрузка'} +function dayReport(k,list){ + let lines=['Дневник питания — '+dayTitle(k),'Сформирован: '+new Date().toLocaleString('ru-RU'),'Приёмов пищи: '+list.length]; + let p=dayLoadPct(list),s=daySummary(list); + lines.push('Совокупная нагрузка за день: '+p+'% — '+loadWord(p)); + if(s.groups.length)lines.push('FODMAP-группы за день: '+s.groups.join(', ')); + lines.push(''); + [...list].sort((a,b)=>mealDateTime(a)-mealDateTime(b)).forEach((e,i)=>{ + lines.push((i+1)+'. '+mealDateLabel(e)); + lines.push('Состав: '+(e.meal||[]).map(m=>m.name+' — '+m.g+' г').join(', ')); + let rs=e.reactions||[]; + if(!rs.length)lines.push('Реакции: не отмечены.'); + else{lines.push('Реакции ('+rs.length+'):');rs.forEach((r,j)=>{let d=[r.severity+'/10 — '+(r.severityLabel||severityText[r.severity]||'без описания'),r.delay];if(r.types&&r.types.length)d.push(r.types.join(', '));lines.push(' '+(j+1)+') '+new Date(r.createdAt).toLocaleString('ru-RU')+': '+d.filter(Boolean).join(' • '));if(r.comment)lines.push(' Комментарий: '+r.comment)})} + lines.push(''); + }); + return lines.join('\n').trim(); +} +function downloadDay(k){ + let list=getEntries().filter(e=>dayKey(e)===k); + if(!list.length)return; + let blob=new Blob([dayReport(k,list)],{type:'text/plain;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a'); + a.href=url;a.download='fodmap-'+k+'.txt';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000); +} function dayKey(e){let d=mealDateTime(e);return localDateString(d)} function dayTitle(k){ let d=new Date(k+'T12:00:00'),today=localDateString(),y=new Date();y.setDate(y.getDate()-1); @@ -672,11 +702,12 @@ function showHistory(){ entries.forEach(e=>{let k=dayKey(e);if(!days.has(k))days.set(k,[]);days.get(k).push(e)}); let today=localDateString(); h.innerHTML=[...days.entries()].map(([k,list])=>{ - let s=daySummary(list); + let s=daySummary(list),p=dayLoadPct(list); let open=(openDays&&k in openDays)?!!openDays[k]:k===today; return `<details class="dayfold" ${open?'open':''} ontoggle="toggleDay('${k}',this)"> - <summary><span class="dayname">${esc(dayTitle(k))}</span><span class="daychips">${s.chips}</span><span class="daymeta">${esc(s.line)}</span></summary> - ${s.groups.length?'<div class="daygroups">'+s.groups.map(g=>'<span class="gchip">'+esc(g)+'</span>').join('')+'</div>':''} + <summary><span class="dayname">${esc(dayTitle(k))}</span><span class="daychips">${s.chips}</span><span class="daymeta">${esc(s.line)}</span> + <span class="dayload"><span class="dayload-track"><span class="dayload-fill" style="width:${Math.min(100,p)}%;background:${loadColor(p)}"></span></span><span class="dayload-num" style="color:${loadColor(p)}">${p}%</span><span class="dayload-word">${esc(loadWord(p))}</span></span></summary> + <div class="dayactions">${s.groups.length?s.groups.map(g=>'<span class="gchip">'+esc(g)+'</span>').join(''):''}<button class="dayexport" onclick="downloadDay('${k}')">⤓ Выгрузить этот день</button></div> <div class="daybody">${list.map(entryHtml).join('')}</div> </details>`; }).join('');