Replace the hello-world shell with the elevated design system, bilingual screens, recipe/extras data, and the interactive Mein Teller view. Co-authored-by: Cursor <cursoragent@cursor.com>
837 lines
46 KiB
HTML
837 lines
46 KiB
HTML
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="apple-mobile-web-app-title" content="Baukasten">
|
||
<meta name="theme-color" content="#ffffff">
|
||
<title>Pocket Pascal</title>
|
||
<style>
|
||
:root{
|
||
--bg:#ffffff; --surface:#ffffff; --ink:#111111; --ink-soft:#5c5c5c; --line:#e6e6e6;
|
||
--green:#2F5233; --green-soft:#e3ece4;
|
||
--sage:#4A6FA5; --sage-soft:#e7edf5;
|
||
--terracotta:#B33D22; --terracotta-soft:#f7e0d8; --terracotta-text:#A83B22;
|
||
--leaf:#A8C23E; --leaf-soft:#f2f5df; --leaf-text:#5C6B1E;
|
||
--gold:#E8B93A; --gold-soft:#fdf1d9; --gold-text:#8A6A1A;
|
||
--slate:#6B4C9A; --slate-soft:#efe9f5;
|
||
--pick:#16A34A; --caution:#DC2626;
|
||
}
|
||
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
|
||
html,body{ height:100%; }
|
||
body{
|
||
margin:0; background:var(--bg); color:var(--ink);
|
||
font-family: ui-sans-serif, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||
font-variant-numeric: tabular-nums;
|
||
padding-top:env(safe-area-inset-top); padding-bottom:env(safe-area-inset-bottom);
|
||
overscroll-behavior-y: contain;
|
||
}
|
||
.display{ font-family:"Futura","Century Gothic","Avenir Next","Avenir",ui-sans-serif,sans-serif; font-weight:800; letter-spacing:-0.01em; }
|
||
|
||
/* ---------- top bar ---------- */
|
||
.topbar{
|
||
position:sticky; top:0; z-index:10; background:var(--surface);
|
||
border-bottom:1px solid var(--line);
|
||
display:flex; align-items:center; gap:8px;
|
||
padding:14px 16px calc(14px + env(safe-area-inset-top)) 16px;
|
||
margin-top:calc(-1 * env(safe-area-inset-top));
|
||
padding-top:calc(14px + env(safe-area-inset-top));
|
||
}
|
||
.backbtn{
|
||
display:none; border:none; background:var(--bg); color:var(--ink);
|
||
width:34px; height:34px; border-radius:50%; font-size:1.1rem;
|
||
align-items:center; justify-content:center; flex:none;
|
||
}
|
||
.backbtn.show{ display:flex; }
|
||
.tb-title{ flex:1; min-width:0; }
|
||
.topbar h1{ font-size:1.05rem; margin:0; font-weight:900; text-transform:uppercase; letter-spacing:0.02em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.topbar .eyebrow{ font-size:0.62rem; text-transform:uppercase; letter-spacing:0.1em; color:var(--ink-soft); font-weight:700; display:block; }
|
||
.tb-actions{ flex:none; display:flex; align-items:center; gap:8px; }
|
||
.langpill{
|
||
display:flex; gap:2px; background:var(--bg); border:1px solid var(--line);
|
||
border-radius:999px; padding:2px;
|
||
}
|
||
.langpill button{
|
||
border:none; background:transparent; padding:5px 9px; border-radius:999px;
|
||
font-size:0.66rem; font-weight:800; letter-spacing:0.04em; color:var(--ink-soft);
|
||
}
|
||
.langpill button.active{ background:var(--green); color:#fff; }
|
||
.gearbtn{
|
||
border:none; background:var(--bg); width:30px; height:30px; border-radius:50%;
|
||
font-size:0.95rem; display:flex; align-items:center; justify-content:center; flex:none;
|
||
}
|
||
|
||
main{ padding:16px; padding-bottom:calc(32px + env(safe-area-inset-bottom)); max-width:640px; margin:0 auto; }
|
||
.view{ display:none; }
|
||
.view.active{ display:block; }
|
||
|
||
/* ---------- home grid ---------- */
|
||
.homegrid{
|
||
display:grid; grid-template-columns:repeat(2,1fr); gap:12px;
|
||
grid-auto-rows:minmax(120px,auto);
|
||
}
|
||
.tile{
|
||
border:1px solid var(--line); border-radius:14px; overflow:hidden;
|
||
display:flex; flex-direction:column; justify-content:space-between;
|
||
padding:16px; text-align:left; background:var(--surface);
|
||
-webkit-user-select:none; user-select:none;
|
||
}
|
||
.tile .ic{ font-size:2rem; line-height:1; }
|
||
.tile .t{ font-size:0.95rem; font-weight:900; text-transform:uppercase; letter-spacing:0.01em; margin-top:10px; }
|
||
.tile .d{ font-size:0.7rem; color:var(--ink-soft); margin-top:2px; }
|
||
.tile.soon{ opacity:0.4; }
|
||
.tile.teller{ border-top:4px solid var(--green); }
|
||
.tile.vorrat{ border-top:4px solid var(--terracotta); }
|
||
.tile.settings{ border-top:4px solid var(--slate); }
|
||
.tile.builder{ border-top:4px solid var(--pick); }
|
||
.tile.soon{ border-top:4px solid var(--line); }
|
||
|
||
/* ---------- plate toggle ---------- */
|
||
.segctl{
|
||
display:flex; background:var(--green-soft); border-radius:999px; padding:3px; margin-bottom:16px;
|
||
}
|
||
.segctl button{
|
||
flex:1; border:none; background:transparent; padding:9px 4px; border-radius:999px;
|
||
font-size:0.78rem; font-weight:700; color:var(--ink-soft);
|
||
}
|
||
.segctl button.active{ background:var(--surface); color:var(--ink); box-shadow:0 1px 3px rgba(0,0,0,0.12); }
|
||
|
||
.plate-wrap{ display:flex; flex-direction:column; align-items:center; gap:10px; margin-bottom:18px; }
|
||
.plate{ width:150px; height:150px; border-radius:50%; position:relative; box-shadow:inset 0 0 0 1px var(--line); }
|
||
.plate::after{ content:""; position:absolute; inset:22%; background:var(--surface); border-radius:50%; box-shadow:inset 0 0 0 1px var(--line); }
|
||
.legend{ display:flex; flex-direction:column; gap:4px; font-size:0.78rem; color:var(--ink-soft); }
|
||
.legend span{ display:flex; align-items:center; gap:6px; justify-content:center; }
|
||
.dot{ width:8px; height:8px; border-radius:50%; flex:none; }
|
||
.dot.g{background:var(--green);} .dot.t{background:var(--terracotta);} .dot.o{background:var(--gold);} .dot.s{background:var(--slate);}
|
||
|
||
/* ---------- accordion (steps + categories) ---------- */
|
||
.acc{ border:1px solid var(--line); border-radius:10px; overflow:hidden; margin-bottom:10px; }
|
||
.acc-head{
|
||
display:flex; align-items:center; gap:10px; padding:13px 14px;
|
||
background:var(--surface); border:none; width:100%; text-align:left;
|
||
}
|
||
.acc-head .num{ flex:none; width:26px; height:26px; border-radius:50%; color:#fff; font-weight:700; font-size:0.8rem; display:flex; align-items:center; justify-content:center; }
|
||
.acc-head .num.g{background:var(--green);} .acc-head .num.t{background:var(--terracotta);} .acc-head .num.o{background:var(--gold); color:#111;} .acc-head .num.s{background:var(--slate);}
|
||
.acc-head .ic{ flex:none; font-size:1.3rem; width:26px; text-align:center; }
|
||
.acc-head .title{ flex:1; font-weight:800; font-size:0.86rem; text-transform:uppercase; letter-spacing:0.02em; }
|
||
.acc-head .qty{ font-size:0.72rem; color:var(--ink-soft); font-weight:600; }
|
||
.acc-head .count{ font-size:0.7rem; color:var(--ink-soft); background:var(--bg); border-radius:20px; padding:2px 9px; flex:none; }
|
||
.acc-head .chev{ flex:none; font-size:0.9rem; color:var(--ink-soft); transition:transform 0.15s ease; }
|
||
.acc.open .chev{ transform:rotate(90deg); }
|
||
.acc-body{ display:none; padding:0 14px 14px; border-top:1px solid var(--line); }
|
||
.acc.open .acc-body{ display:block; padding-top:10px; }
|
||
.acc-body p{ margin:0; font-size:0.82rem; color:var(--ink-soft); line-height:1.5; }
|
||
|
||
.cat-head{ border-left-width:4px; border-left-style:solid; }
|
||
.acc.protein .cat-head{ border-left-color:var(--green); }
|
||
.acc.snack .cat-head{ border-left-color:var(--sage); }
|
||
.acc.obst .cat-head{ border-left-color:var(--terracotta); }
|
||
.acc.gemuese .cat-head{ border-left-color:var(--leaf); }
|
||
.acc.fett .cat-head{ border-left-color:var(--gold); }
|
||
.acc.kh .cat-head{ border-left-color:var(--slate); }
|
||
|
||
table{ width:100%; border-collapse:collapse; font-size:0.82rem; margin-top:6px; }
|
||
table td{ padding:6px 0; border-bottom:1px dashed var(--line); vertical-align:top; }
|
||
table td:last-child{ text-align:right; color:var(--ink-soft); white-space:nowrap; padding-left:8px; }
|
||
tr:last-child td{ border-bottom:none; }
|
||
.approx{ font-style:italic; }
|
||
.dotmark{ font-size:0.7em; margin-right:3px; }
|
||
.dotmark.pick{ color:var(--pick); }
|
||
.dotmark.caution{ color:var(--caution); }
|
||
.subhead{ font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--ink-soft); margin:10px 0 0; }
|
||
.rot{ margin-top:10px; padding-top:10px; border-top:1px dashed var(--line); font-size:0.78rem; line-height:1.45; }
|
||
.rot b{ text-transform:uppercase; letter-spacing:0.03em; font-size:0.7rem; }
|
||
.acc.protein .rot b{ color:var(--green); } .acc.snack .rot b{ color:var(--sage); }
|
||
.acc.obst .rot b{ color:var(--terracotta-text); } .acc.gemuese .rot b{ color:var(--leaf-text); }
|
||
.acc.fett .rot b{ color:var(--gold-text); } .acc.kh .rot b{ color:var(--slate); }
|
||
|
||
.searchbar{
|
||
display:flex; align-items:center; gap:8px; background:var(--surface); border:1px solid var(--line);
|
||
border-radius:10px; padding:10px 12px; margin-bottom:14px;
|
||
}
|
||
.searchbar input{ border:none; outline:none; font-size:0.9rem; flex:1; background:transparent; color:var(--ink); }
|
||
.searchbar .clear{ border:none; background:var(--bg); color:var(--ink-soft); border-radius:50%; width:22px; height:22px; font-size:0.75rem; display:none; }
|
||
|
||
.unitnote{ font-size:0.74rem; color:var(--ink-soft); background:var(--green-soft); border-radius:8px; padding:9px 12px; margin-bottom:14px; }
|
||
.recipe{ background:var(--sage-soft); border:1px solid var(--sage); border-radius:8px; padding:10px 12px; font-size:0.8rem; line-height:1.5; margin-top:8px; }
|
||
.recipe b{ color:var(--sage); }
|
||
.rules{ display:flex; flex-direction:column; gap:8px; border-top:1px solid var(--line); padding-top:14px; margin-top:16px; }
|
||
.rule{ display:flex; gap:8px; font-size:0.82rem; line-height:1.4; }
|
||
.rule::before{ content:"—"; color:var(--terracotta-text); flex:none; }
|
||
|
||
/* ---------- settings ---------- */
|
||
.setgroup{ border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin-bottom:12px; }
|
||
.setgroup h3{ font-size:0.72rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--ink-soft); margin:0 0 10px; font-weight:700; }
|
||
.setrow{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||
.setrow .label{ font-size:0.88rem; font-weight:600; }
|
||
.setnote{ font-size:0.72rem; color:var(--ink-soft); margin-top:8px; line-height:1.4; }
|
||
.setnote.ok{ color:var(--pick); }
|
||
.setnote.warn{ color:var(--caution); }
|
||
.setgroup.disabled{ opacity:0.45; }
|
||
|
||
/* ---------- builder ---------- */
|
||
.builder-row{ margin-bottom:16px; }
|
||
.builder-row .rowhead{ display:flex; align-items:baseline; justify-content:space-between; margin-bottom:8px; }
|
||
.builder-row label{ font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--ink-soft); }
|
||
.builder-row .rcount{ font-size:0.68rem; color:var(--ink-soft); }
|
||
.chip-grid{ display:flex; flex-wrap:wrap; gap:7px; }
|
||
.chip{
|
||
border:1px solid var(--line); background:var(--surface); color:var(--ink);
|
||
padding:8px 13px; border-radius:999px; font-size:0.82rem; font-weight:600;
|
||
display:flex; align-items:center; gap:5px; -webkit-user-select:none; user-select:none;
|
||
}
|
||
.chip .dotmark{ margin-right:0; }
|
||
.builder-row.protein .chip.on{ background:var(--green); border-color:var(--green); color:#fff; }
|
||
.builder-row.obstgem .chip.on{ background:var(--terracotta); border-color:var(--terracotta); color:#fff; }
|
||
.builder-row.fett .chip.on{ background:var(--gold); border-color:var(--gold); color:#111; }
|
||
.builder-row.kh .chip.on{ background:var(--slate); border-color:var(--slate); color:#fff; }
|
||
.chip.on .dotmark.pick, .chip.on .dotmark.caution{ filter:brightness(1.4) saturate(1.4); }
|
||
|
||
.builder-result{ margin-top:6px; padding:14px 16px; border-radius:10px; border:1px solid var(--line); font-size:0.85rem; line-height:1.5; }
|
||
.builder-result .bh{ font-weight:900; font-size:0.95rem; text-transform:uppercase; letter-spacing:0.01em; margin-bottom:4px; display:flex; align-items:center; gap:8px; }
|
||
.builder-result.neutral{ background:var(--surface); }
|
||
.builder-result.solid{ background:var(--green-soft); }
|
||
.builder-result.solid .bh{ color:var(--green); }
|
||
.builder-result.pick{ background:var(--green-soft); }
|
||
.builder-result.pick .bh{ color:var(--green); }
|
||
.builder-result.caution{ background:#fdf1ec; }
|
||
.builder-result.caution .bh{ color:var(--terracotta-text); }
|
||
.builder-result ul{ margin:6px 0 0; padding-left:18px; }
|
||
.builder-result li{ margin-bottom:2px; }
|
||
</style>
|
||
|
||
<div class="topbar">
|
||
<button class="backbtn" id="backBtn" onclick="goHome()">‹</button>
|
||
<div class="tb-title">
|
||
<span class="eyebrow" id="topEyebrow">Body Comeback</span>
|
||
<h1 id="topTitle">Pocket Pascal</h1>
|
||
</div>
|
||
<div class="tb-actions">
|
||
<div class="langpill">
|
||
<button id="pillDe" onclick="setLang('de')">DE</button>
|
||
<button id="pillEn" onclick="setLang('en')">EN</button>
|
||
</div>
|
||
<button class="gearbtn" onclick="showView('settings')">⚙️</button>
|
||
</div>
|
||
</div>
|
||
|
||
<main>
|
||
|
||
<!-- HOME -->
|
||
<section class="view active" id="view-home">
|
||
<div class="homegrid" id="homegrid"></div>
|
||
</section>
|
||
|
||
<!-- TELLER -->
|
||
<section class="view" id="view-teller">
|
||
<div class="segctl">
|
||
<button id="segFull" class="active" onclick="setMeal('full')"></button>
|
||
<button id="segHalf" onclick="setMeal('half')"></button>
|
||
</div>
|
||
|
||
<div class="plate-wrap">
|
||
<div class="plate" id="plateFull" style="background:conic-gradient(var(--green) 0deg 120deg, var(--terracotta) 120deg 240deg, var(--gold) 240deg 300deg, var(--slate) 300deg 360deg);"></div>
|
||
<div class="plate" id="plateHalf" style="display:none; background:conic-gradient(var(--green) 0deg 200deg, var(--terracotta) 200deg 360deg);"></div>
|
||
<div class="legend" id="legendFull"></div>
|
||
<div class="legend" id="legendHalf" style="display:none;"></div>
|
||
</div>
|
||
|
||
<div id="stepsList"></div>
|
||
|
||
<div class="rules" id="tellerRules"></div>
|
||
</section>
|
||
|
||
<!-- VORRATSKAMMER -->
|
||
<section class="view" id="view-vorrat">
|
||
<div class="searchbar">
|
||
<span>🔍</span>
|
||
<input id="searchInput" oninput="onSearch()">
|
||
<button class="clear" id="clearBtn" onclick="clearSearch()">✕</button>
|
||
</div>
|
||
<div class="unitnote" id="unitnote"></div>
|
||
<div id="catList"></div>
|
||
</section>
|
||
|
||
<!-- BUILDER -->
|
||
<section class="view" id="view-builder">
|
||
<div class="segctl">
|
||
<button id="bSegFull" class="active" onclick="setBuilderMeal('full')"></button>
|
||
<button id="bSegHalf" onclick="setBuilderMeal('half')"></button>
|
||
</div>
|
||
<div id="builderPickers"></div>
|
||
<div class="builder-result neutral" id="builderResult"></div>
|
||
</section>
|
||
|
||
<!-- SETTINGS -->
|
||
<section class="view" id="view-settings">
|
||
<div class="setgroup">
|
||
<h3 id="setLangLabel">Sprache</h3>
|
||
<div class="setrow">
|
||
<span class="label">Deutsch / English</span>
|
||
<div class="langpill">
|
||
<button id="pillDe2" onclick="setLang('de')">DE</button>
|
||
<button id="pillEn2" onclick="setLang('en')">EN</button>
|
||
</div>
|
||
</div>
|
||
<div class="setnote" id="persistNote"></div>
|
||
</div>
|
||
<div class="setgroup disabled">
|
||
<h3 id="setMoreLabel">Weitere Einstellungen</h3>
|
||
<div class="setnote" id="setMoreNote"></div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<script>
|
||
// ---------------------------------------------------------------------------
|
||
// i18n dictionary — all static UI strings
|
||
// ---------------------------------------------------------------------------
|
||
const UI = {
|
||
de: {
|
||
body:'Body Comeback', appTitle:'Pocket Pascal',
|
||
tileTellerT:'Dein Teller', tileTellerD:'Baukasten & Reihenfolge',
|
||
tileVorratT:'Vorratskammer', tileVorratD:'Einkauf & Kühlschrank',
|
||
tileSettingsT:'Einstellungen', tileSettingsD:'Sprache & mehr',
|
||
tileBuilderT:'Teller-Builder', tileBuilderD:'Kombi checken',
|
||
tileSugarT:'Zucker-Guide', tileSugarD:'Bald verfügbar',
|
||
tileCheckT:'Checkliste', tileCheckD:'Bald verfügbar',
|
||
card1:'Karte 1', card2:'Karte 2', cardSettings:'Einstellungen', cardBuilder:'Teller-Builder',
|
||
pickProtein:'Protein', pickObstGem:'Obst / Gemüse', pickFett:'Fett', pickKh:'Kohlenhydrate',
|
||
pickPlaceholder:'— auswählen —',
|
||
bResultMissingH:'Noch unvollständig',
|
||
bResultMissingList:'Fehlt noch:',
|
||
bResultSolidH:'Solider Teller',
|
||
bResultSolidHSnack:'Solider Snack',
|
||
bResultPickH:'Guter Teller!',
|
||
bResultPickHSnack:'Guter Snack!',
|
||
bResultPickList:'Besonders gut gewählt:',
|
||
bResultCautionH:'Mit Hinweis',
|
||
bResultCautionList:'Bewusst genießen:',
|
||
bResultCautionNote:'Kein Fehler — nur ein Punkt, den du im Blick behalten kannst.',
|
||
segFull:'Ganze Mahlzeit', segHalf:'Halbe Mahlzeit',
|
||
legFullP:'⅓ Protein', legFullVO:'⅓ Gemüse / Obst', legFullFK:'⅓ Fett + Kohlenhydrate',
|
||
legHalfP:'Protein', legHalfVO:'Gemüse / Obst',
|
||
rule1:'Jede Mahlzeit beruht auf Protein — egal ob süß, herzhaft, Snack oder Hauptmahlzeit.',
|
||
rule2:'Jede Mahlzeit enthält eine Portion Gemüse oder Obst.',
|
||
rule3:'4 Mahlzeiten am Tag, ca. alle 3–4 Std.',
|
||
rule4:'2 L Wasser täglich.',
|
||
searchPlaceholder:'Lebensmittel suchen…',
|
||
unitnote:'Werte je 100 g, sofern nicht anders angegeben.',
|
||
pickLabel:'besonders empfehlenswert', cautionLabel:'bewusst genießen',
|
||
noResults:'Keine Treffer.',
|
||
thumbDefault:'Daumenregel',
|
||
setLangLabel:'Sprache', setMoreLabel:'Weitere Einstellungen',
|
||
setMoreNote:'Mahlzeiten/Tag, Einkaufsrhythmus u.a. folgen in Phase 2.',
|
||
persistOk:'Spracheinstellung wird auf diesem Gerät gespeichert.',
|
||
persistWarn:'Spracheinstellung wird in dieser Ansicht nicht gespeichert — nach Neuladen wieder Deutsch.'
|
||
},
|
||
en: {
|
||
body:'Body Comeback', appTitle:'Pocket Pascal',
|
||
tileTellerT:'Your Plate', tileTellerD:'Building blocks & sequence',
|
||
tileVorratT:'The Pantry', tileVorratD:'Shopping & fridge',
|
||
tileSettingsT:'Settings', tileSettingsD:'Language & more',
|
||
tileBuilderT:'Plate Builder', tileBuilderD:'Check a combo',
|
||
tileSugarT:'Sugar Guide', tileSugarD:'Coming soon',
|
||
tileCheckT:'Checklist', tileCheckD:'Coming soon',
|
||
card1:'Card 1', card2:'Card 2', cardSettings:'Settings', cardBuilder:'Plate Builder',
|
||
pickProtein:'Protein', pickObstGem:'Veg / Fruit', pickFett:'Fat', pickKh:'Carbohydrate',
|
||
pickPlaceholder:'— choose —',
|
||
bResultMissingH:'Still incomplete',
|
||
bResultMissingList:'Still missing:',
|
||
bResultSolidH:'Solid plate',
|
||
bResultSolidHSnack:'Solid snack',
|
||
bResultPickH:'Good plate!',
|
||
bResultPickHSnack:'Good snack!',
|
||
bResultPickList:'Especially well chosen:',
|
||
bResultCautionH:'With a note',
|
||
bResultCautionList:'Enjoy mindfully:',
|
||
bResultCautionNote:'Not a mistake — just something to keep an eye on.',
|
||
segFull:'Full meal', segHalf:'Half meal',
|
||
legFullP:'⅓ Protein', legFullVO:'⅓ Veg / Fruit', legFullFK:'⅓ Fat + Carbs',
|
||
legHalfP:'Protein', legHalfVO:'Veg / Fruit',
|
||
rule1:'Every meal is built on protein — sweet, savoury, snack, or main.',
|
||
rule2:'Every meal includes a portion of vegetables or fruit.',
|
||
rule3:'4 meals a day, roughly every 3–4 hours.',
|
||
rule4:'2 L water daily.',
|
||
searchPlaceholder:'Search foods…',
|
||
unitnote:'Values per 100 g unless noted otherwise.',
|
||
pickLabel:'especially recommended', cautionLabel:'enjoy mindfully',
|
||
noResults:'No matches.',
|
||
thumbDefault:'Rule of thumb',
|
||
setLangLabel:'Language', setMoreLabel:'More settings',
|
||
setMoreNote:'Meals/day, shopping rhythm and more arrive in Phase 2.',
|
||
persistOk:'Language choice is saved on this device.',
|
||
persistWarn:'Language choice isn’t saved in this view — reloads back to German.'
|
||
}
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// data model — structured records, name/text as {de,en}
|
||
// ---------------------------------------------------------------------------
|
||
const STEPS = [
|
||
{n:1, cls:'g',
|
||
title:{de:'Protein', en:'Protein'},
|
||
qty:{de:'30 g pro Mahlzeit', en:'30 g per meal'},
|
||
text:{de:'Bspw. 125 g Rinderfilet, 300 g Skyr, 40 g Proteinpulver, 50 g Erbsenproteingeschnetzeltes oder 5 Eier. 150 g am Tag insgesamt.',
|
||
en:'E.g. 125 g beef fillet, 300 g Skyr, 40 g protein powder, 50 g pea-protein strips, or 5 eggs. 150 g total per day.'}},
|
||
{n:2, cls:'t',
|
||
title:{de:'Obst ODER Gemüse', en:'Fruit OR Vegetables'},
|
||
qty:{de:'eine Portion', en:'one portion'},
|
||
text:{de:'Obst = 100 g oder eine geballte Faust. Gemüse = 200 g oder 2 Handvoll.',
|
||
en:'Fruit = 100 g or one clenched fist. Vegetables = 200 g or 2 handfuls.'}},
|
||
{n:3, cls:'o',
|
||
title:{de:'Fett', en:'Fat'},
|
||
qty:{de:'', en:''},
|
||
text:{de:'15 g Samen/Kerne/Nüsse (≈20 Stück, 1 EL) · ¼ Avocado · 1 EL Öl (15 g) — Nussöl außer Erdnuss, Olivenöl, Leinsamenöl.',
|
||
en:'15 g seeds/nuts (≈20 pieces, 1 tbsp) · ¼ avocado · 1 tbsp oil (15 g) — nut oil except peanut, olive oil, linseed oil.'}},
|
||
{n:4, cls:'s',
|
||
title:{de:'Kohlenhydrate', en:'Carbohydrates'},
|
||
qty:{de:'', en:''},
|
||
text:{de:'75 g Reis/Nudeln/Brot (trocken) · ca. 250 g rohe Kartoffeln (≈3 mittelgroße) · 50 g Haferflocken (≈5 gehäufte EL).',
|
||
en:'75 g rice/pasta/bread (dry) · approx. 250 g raw potatoes (≈3 medium) · 50 g oats (≈5 heaped tbsp).'}}
|
||
];
|
||
|
||
// f: 'pick' | 'caution' | undefined
|
||
const CATS = [
|
||
{id:'protein', cls:'protein', ic:'🥚', title:{de:'Protein', en:'Protein'},
|
||
thumb:{de:'Mindestens 15–20 g Protein pro 100 g, wenig Zucker & wenig Fett, kurze Zutatenliste.',
|
||
en:'At least 15–20 g protein per 100 g, low sugar & low fat, short ingredient list.'},
|
||
items:[
|
||
{id:'edamame', name:{de:'Edamame', en:'Edamame'}, v:{de:'125 kcal · 14 g', en:'125 kcal · 14 g'}},
|
||
{id:'egg', name:{de:'Ei (Größe M)', en:'Egg (size M)'}, v:{de:'137 kcal · 13 g', en:'137 kcal · 13 g'}},
|
||
{id:'pea_protein_strips', name:{de:'Erbsenproteinschnetzel', en:'Pea-protein strips'}, v:{de:'367 kcal · 61 g', en:'367 kcal · 61 g'}},
|
||
{id:'feta', name:{de:'Feta (leicht)', en:'Feta (light)'}, v:{de:'161 kcal · 19 g', en:'161 kcal · 19 g'}, f:'caution'},
|
||
{id:'chicken_breast', name:{de:'Hühnerbrust', en:'Chicken breast'}, v:{de:'111 kcal · 24 g', en:'111 kcal · 24 g'}, f:'pick'},
|
||
{id:'cottage_cheese', name:{de:'Hüttenkäse', en:'Cottage cheese'}, v:{de:'87 kcal · 12,3 g', en:'87 kcal · 12.3 g'}},
|
||
{id:'like_chicken', name:{de:'„Like Chicken" (pflanzlich)', en:'"Like Chicken" (plant-based)'}, v:{de:'≈140 kcal · 18 g', en:'≈140 kcal · 18 g'}, approx:true},
|
||
{id:'low_fat_quark', name:{de:'Magerquark', en:'Low-fat quark'}, v:{de:'67 kcal · 12 g', en:'67 kcal · 12 g'}},
|
||
{id:'beef_fillet', name:{de:'Rinderfilet / Tatar', en:'Beef fillet / tartare'}, v:{de:'112 kcal · 22 g', en:'112 kcal · 22 g'}, f:'pick'},
|
||
{id:'silken_tofu', name:{de:'Seidentofu', en:'Silken tofu'}, v:{de:'≈55 kcal · 5 g', en:'≈55 kcal · 5 g'}, approx:true},
|
||
{id:'skyr', name:{de:'Skyr', en:'Skyr'}, v:{de:'54 kcal · 10,3 g', en:'54 kcal · 10.3 g'}, f:'pick'},
|
||
{id:'tempeh', name:{de:'Tempeh', en:'Tempeh'}, v:{de:'184 kcal · 19 g', en:'184 kcal · 19 g'}},
|
||
{id:'tofu', name:{de:'Tofu', en:'Tofu'}, v:{de:'129 kcal · 13 g', en:'129 kcal · 13 g'}}
|
||
],
|
||
sub:{title:{de:'Milch & Joghurt', en:'Milk & yoghurt'}, items:[
|
||
{id:'greek_yogurt_10', name:{de:'Griech. Joghurt 10 %', en:'Greek yoghurt 10%'}, v:{de:'≈115 kcal · 5 g', en:'≈115 kcal · 5 g'}, approx:true},
|
||
{id:'natural_yogurt', name:{de:'Naturjoghurt', en:'Natural yoghurt'}, v:{de:'≈61 kcal · 3,5 g', en:'≈61 kcal · 3.5 g'}, approx:true},
|
||
{id:'oatly_barista', name:{de:'Oatly Barista', en:'Oatly Barista'}, v:{de:'≈68 kcal · 1 g (kaum Protein)', en:'≈68 kcal · 1 g (barely protein)'}, approx:true, f:'caution'},
|
||
{id:'whole_milk', name:{de:'Vollmilch 3,5 %', en:'Whole milk 3.5%'}, v:{de:'≈64 kcal · 3,4 g', en:'≈64 kcal · 3.4 g'}, approx:true, f:'caution'}
|
||
]}},
|
||
{id:'snack', cls:'snack', ic:'🥤', title:{de:'Proteinsnacks für Steffi', en:'Protein snacks for Steffi'},
|
||
thumb:{de:'Mindestens 15 g Protein pro Portion, unter 5 g Zucker, Fett unter 8 g — Zutatenliste beginnt nicht mit Zucker/Sirup/Schokolade.',
|
||
en:'At least 15 g protein per serving, under 5 g sugar, fat under 8 g — ingredient list shouldn’t open with sugar/syrup/chocolate.'},
|
||
items:[
|
||
{id:'three_eggs', name:{de:'3 Eier', en:'3 eggs'}, v:{de:'≈205 kcal · 19 g', en:'≈205 kcal · 19 g'}, approx:true},
|
||
{id:'cottage_berries', name:{de:'Hüttenkäse + Beeren', en:'Cottage cheese + berries'}, v:{de:'≈210 kcal · 25 g', en:'≈210 kcal · 25 g'}, approx:true},
|
||
{id:'chickpea_waffles', name:{de:'Kichererbsenwaffeln 90g', en:'Chickpea waffles 90g'}, v:{de:'≈300 kcal · 17 g', en:'≈300 kcal · 17 g'}, approx:true, f:'caution'},
|
||
{id:'pumpkin_seeds_30', name:{de:'Kürbiskerne 30g', en:'Pumpkin seeds 30g'}, v:{de:'≈171 kcal · 9 g', en:'≈171 kcal · 9 g'}, approx:true, f:'caution'},
|
||
{id:'lentil_waffles', name:{de:'Linsenwaffeln 90g', en:'Lentil waffles 90g'}, v:{de:'≈300 kcal · 18 g', en:'≈300 kcal · 18 g'}, approx:true, f:'caution'},
|
||
{id:'protein_bar', name:{de:'Magerer Proteinriegel', en:'Lean protein bar'}, v:{de:'≈180 kcal · 20 g', en:'≈180 kcal · 20 g'}, approx:true},
|
||
{id:'protein_rice_pudding', name:{de:'Proteinmilchreis (2x)', en:'Protein rice pudding (2x)'}, v:{de:'≈340 kcal · 40 g', en:'≈340 kcal · 40 g'}, approx:true},
|
||
{id:'protein_pudding', name:{de:'Proteinpudding', en:'Protein pudding'}, v:{de:'≈90 kcal · 20 g', en:'≈90 kcal · 20 g'}, approx:true, f:'pick'},
|
||
{id:'protein_shake', name:{de:'Proteinshake 40g', en:'Protein shake 40g'}, v:{de:'≈150 kcal · 30 g', en:'≈150 kcal · 30 g'}, approx:true, f:'pick'},
|
||
{id:'turkey_breast_pack', name:{de:'Putenbrust, 1 Packung', en:'Turkey breast, 1 pack'}, v:{de:'≈130 kcal · 28 g', en:'≈130 kcal · 28 g'}, approx:true, f:'pick'},
|
||
{id:'smoked_tofu_cubes', name:{de:'Räuchertofu-Würfel', en:'Smoked tofu cubes'}, v:{de:'≈225 kcal · 25 g', en:'≈225 kcal · 25 g'}, approx:true},
|
||
{id:'skyr_honey', name:{de:'Skyr/Griech. Joghurt 10% + Honig, 250g+', en:'Skyr/Greek yoghurt 10% + honey, 250g+'}, v:{de:'≈180 kcal · 25 g', en:'≈180 kcal · 25 g'}, approx:true}
|
||
],
|
||
recipe:{title:{de:'Rezept: Whey-Waffeln', en:'Recipe: Whey waffles'},
|
||
text:{de:'40 g Whey-Protein + 1 Ei + 40 g Haferflocken (fein gemahlen) + 60 ml Milch/Wasser verquirlen. ≈25 g Protein.',
|
||
en:'Whisk 40 g whey protein + 1 egg + 40 g oats (finely ground) + 60 ml milk/water. ≈25 g protein.'}}},
|
||
{id:'obst', cls:'obst', ic:'🍎', title:{de:'Obst', en:'Fruit'},
|
||
thumb:{de:'Frisch oder tiefgekühlt ohne Zuckerzusatz, Saison bevorzugen.',
|
||
en:'Fresh or frozen without added sugar, prefer what’s in season.'},
|
||
items:[
|
||
{id:'apple', name:{de:'Apfel', en:'Apple'}, v:{de:'Vit. C, Kalium, Kupfer', en:'Vit. C, potassium, copper'}},
|
||
{id:'banana', name:{de:'Banane', en:'Banana'}, v:{de:'Kalium, B6, C, Magnesium', en:'Potassium, B6, C, magnesium'}, f:'caution'},
|
||
{id:'blueberry', name:{de:'Blaubeere', en:'Blueberry'}, v:{de:'Vit. K, C, Mangan', en:'Vit. K, C, manganese'}, f:'pick'},
|
||
{id:'strawberry', name:{de:'Erdbeere', en:'Strawberry'}, v:{de:'Vit. C, Mangan, Folsäure, Kalium', en:'Vit. C, manganese, folate, potassium'}},
|
||
{id:'granadilla', name:{de:'Granadilla (≈ Passionsfrucht)', en:'Granadilla (≈ passion fruit)'}, v:{de:'Vit. C, A, Kalium, B2', en:'Vit. C, A, potassium, B2'}, approx:true},
|
||
{id:'pomegranate', name:{de:'Granatapfel', en:'Pomegranate'}, v:{de:'Vit. C, K, Folsäure, Kalium, B6', en:'Vit. C, K, folate, potassium, B6'}, f:'pick'},
|
||
{id:'mango', name:{de:'Mango', en:'Mango'}, v:{de:'Vit. C, A, B6, Folsäure, Kupfer', en:'Vit. C, A, B6, folate, copper'}, f:'pick'},
|
||
{id:'nectarine', name:{de:'Nektarine', en:'Nectarine'}, v:{de:'Vit. C, A, Kalium, E, Niacin', en:'Vit. C, A, potassium, E, niacin'}},
|
||
{id:'orange', name:{de:'Orange', en:'Orange'}, v:{de:'Vit. C, Folsäure, Kalium, Calcium', en:'Vit. C, folate, potassium, calcium'}},
|
||
{id:'passion_fruit', name:{de:'Passionsfrucht', en:'Passion fruit'}, v:{de:'Vit. C, A, Kalium, Eisen', en:'Vit. C, A, potassium, iron'}, approx:true},
|
||
{id:'plum', name:{de:'Pflaume', en:'Plum'}, v:{de:'Vit. C, K, Kalium, A, Kupfer', en:'Vit. C, K, potassium, A, copper'}},
|
||
{id:'grapes', name:{de:'Trauben', en:'Grapes'}, v:{de:'Vit. K, C, Kalium, Kupfer, Mangan', en:'Vit. K, C, potassium, copper, manganese'}, f:'caution'}
|
||
]},
|
||
{id:'gemuese', cls:'gemuese', ic:'🥦', title:{de:'Gemüse', en:'Vegetables'},
|
||
thumbLabel:{de:'Tipp', en:'Tip'},
|
||
thumb:{de:'TK-Gemüse ist völlig okay, wenn’s schnell gehen muss — genauso nährstoffreich wie frisch, einfach ohne Salz-/Fettzusatz wählen.',
|
||
en:'Frozen veg is perfectly fine when time is short — just as nutrient-rich as fresh, just pick it without added salt/fat.'},
|
||
items:[
|
||
{id:'broccoli', name:{de:'Brokkoli', en:'Broccoli'}, v:{de:'Vit. C, K, Folsäure, Eisen', en:'Vit. C, K, folate, iron'}},
|
||
{id:'mushrooms', name:{de:'Champignons', en:'Mushrooms'}, v:{de:'Kalium, Selen, Kupfer, B2', en:'Potassium, selenium, copper, B2'}},
|
||
{id:'iceberg_lettuce', name:{de:'Eisbergsalat', en:'Iceberg lettuce'}, v:{de:'Vit. K, A, Folsäure, Kalium — nährstoffarm', en:'Vit. K, A, folate, potassium — low nutrient density'}, f:'caution'},
|
||
{id:'lambs_lettuce', name:{de:'Feldsalat', en:'Lamb’s lettuce'}, v:{de:'Vit. A, C, Folsäure, Eisen', en:'Vit. A, C, folate, iron'}, f:'pick'},
|
||
{id:'cucumber', name:{de:'Gurke', en:'Cucumber'}, v:{de:'Vit. K, Kalium, Mangan', en:'Vit. K, potassium, manganese'}},
|
||
{id:'carrot', name:{de:'Karotte', en:'Carrot'}, v:{de:'Vit. A, K, Kalium, B6', en:'Vit. A, K, potassium, B6'}, f:'pick'},
|
||
{id:'bell_pepper', name:{de:'Paprika', en:'Bell pepper'}, v:{de:'Vit. C, A, B6, E', en:'Vit. C, A, B6, E'}},
|
||
{id:'spinach', name:{de:'Spinat', en:'Spinach'}, v:{de:'Vit. K, A, Folsäure, Eisen', en:'Vit. K, A, folate, iron'}, f:'pick'},
|
||
{id:'tomato', name:{de:'Tomate', en:'Tomato'}, v:{de:'Vit. C, K, Kalium, Folsäure', en:'Vit. C, K, potassium, folate'}},
|
||
{id:'zucchini', name:{de:'Zucchini', en:'Zucchini'}, v:{de:'Vit. C, Kalium, B6, Mangan', en:'Vit. C, potassium, B6, manganese'}}
|
||
],
|
||
note:{de:'Hinweis: Süßkartoffel, Kochbanane und Erbsen zählen hier als Kohlenhydratquelle, Avocado als Fettquelle — siehe dort.',
|
||
en:'Note: sweet potato, plantain and peas count here as a carb source, avocado as a fat source — see those cards.'}},
|
||
{id:'fett', cls:'fett', ic:'🥑', title:{de:'Fett', en:'Fat'},
|
||
thumb:{de:'Native/kaltgepresste Öle, ungesalzene & ungeröstete Nüsse/Kerne, Zutatenliste = nur die Zutat selbst.',
|
||
en:'Native/cold-pressed oils, unsalted & unroasted nuts/seeds, ingredient list = just the ingredient itself.'},
|
||
items:[
|
||
{id:'avocado', name:{de:'Avocado', en:'Avocado'}, v:{de:'217 kcal · 12,5 g', en:'217 kcal · 12.5 g'}},
|
||
{id:'chia_seeds', name:{de:'Chiasamen', en:'Chia seeds'}, v:{de:'486 kcal · 31 g', en:'486 kcal · 31 g'}, f:'pick'},
|
||
{id:'pumpkin_seeds', name:{de:'Kürbiskerne', en:'Pumpkin seeds'}, v:{de:'570 kcal · 44 g', en:'570 kcal · 44 g'}},
|
||
{id:'linseed_oil', name:{de:'Leinöl', en:'Linseed oil'}, v:{de:'837 kcal · 93 g', en:'837 kcal · 93 g'}, f:'pick'},
|
||
{id:'almonds', name:{de:'Mandeln', en:'Almonds'}, v:{de:'612 kcal · 55 g', en:'612 kcal · 55 g'}},
|
||
{id:'olive_oil', name:{de:'Olivenöl', en:'Olive oil'}, v:{de:'857 kcal · 91,5 g', en:'857 kcal · 91.5 g'}},
|
||
{id:'walnuts', name:{de:'Walnüsse', en:'Walnuts'}, v:{de:'674 kcal · 62,5 g', en:'674 kcal · 62.5 g'}, f:'pick'}
|
||
]},
|
||
{id:'kh', cls:'kh', ic:'🌾', title:{de:'Kohlenhydrate', en:'Carbohydrates'},
|
||
thumb:{de:'Vollkorn als erste Zutat, wenig zugesetzter Zucker. Faustregel 10:1 — pro 10 g Kohlenhydrate mind. 1 g Ballaststoffe (z. B. 30 g KH → mind. 3 g Ballaststoffe).',
|
||
en:'Whole grain as the first ingredient, low added sugar. 10:1 rule of thumb — at least 1 g fibre per 10 g carbs (e.g. 30 g carbs → at least 3 g fibre).'},
|
||
items:[
|
||
{id:'brown_lentils', name:{de:'Berglinsen', en:'Brown lentils'}, v:{de:'319 kcal · ≈46 g', en:'319 kcal · ≈46 g'}, approx:true, f:'pick'},
|
||
{id:'buckwheat', name:{de:'Buchweizen', en:'Buckwheat'}, v:{de:'343 kcal · 71,5 g', en:'343 kcal · 71.5 g'}},
|
||
{id:'puffed_spelt', name:{de:'Dinkel gepufft', en:'Puffed spelt'}, v:{de:'≈338 kcal · 70 g', en:'≈338 kcal · 70 g'}, approx:true},
|
||
{id:'spelt_pasta', name:{de:'Dinkel-Vollkorn-Nudeln', en:'Whole-grain spelt pasta'}, v:{de:'≈338 kcal · 70 g', en:'≈338 kcal · 70 g'}, approx:true},
|
||
{id:'peas', name:{de:'Erbsen', en:'Peas'}, v:{de:'93 kcal · 10,4 g', en:'93 kcal · 10.4 g'}},
|
||
{id:'oats', name:{de:'Haferflocken', en:'Oats'}, v:{de:'372 kcal · 66,3 g', en:'372 kcal · 66.3 g'}},
|
||
{id:'millet', name:{de:'Hirse', en:'Millet'}, v:{de:'363 kcal · 69 g', en:'363 kcal · 69 g'}, f:'caution'},
|
||
{id:'chickpeas', name:{de:'Kichererbsen', en:'Chickpeas'}, v:{de:'108 kcal · 14,2 g', en:'108 kcal · 14.2 g'}},
|
||
{id:'chickpea_pasta', name:{de:'Kichererbsennudeln / -couscous', en:'Chickpea pasta / couscous'}, v:{de:'≈335 kcal · 50 g', en:'≈335 kcal · 50 g'}, approx:true},
|
||
{id:'plantain', name:{de:'Kochbanane', en:'Plantain'}, v:{de:'≈122 kcal · 32 g', en:'≈122 kcal · 32 g'}, approx:true, f:'caution'},
|
||
{id:'lentil_pasta', name:{de:'Linsennudeln', en:'Lentil pasta'}, v:{de:'≈335 kcal · 52 g', en:'≈335 kcal · 52 g'}, approx:true},
|
||
{id:'quinoa', name:{de:'Quinoa', en:'Quinoa'}, v:{de:'368 kcal · 64,2 g', en:'368 kcal · 64.2 g'}},
|
||
{id:'red_lentils', name:{de:'Rote Linsen', en:'Red lentils'}, v:{de:'326 kcal · ≈52 g', en:'326 kcal · ≈52 g'}, approx:true, f:'pick'},
|
||
{id:'sweet_potato', name:{de:'Süßkartoffel', en:'Sweet potato'}, v:{de:'86 kcal · 20,1 g', en:'86 kcal · 20.1 g'}, f:'caution'},
|
||
{id:'french_lentils', name:{de:'Tellerlinsen', en:'French lentils'}, v:{de:'234 kcal · ≈34 g', en:'234 kcal · ≈34 g'}, approx:true, f:'pick'}
|
||
]}
|
||
];
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// state
|
||
// ---------------------------------------------------------------------------
|
||
let persistWorks = true;
|
||
function loadLang(){
|
||
try{
|
||
const v = localStorage.getItem('pp_lang');
|
||
return v === 'en' ? 'en' : 'de';
|
||
}catch(e){ persistWorks = false; return 'de'; }
|
||
}
|
||
function saveLang(l){
|
||
try{ localStorage.setItem('pp_lang', l); }catch(e){ persistWorks = false; }
|
||
}
|
||
const state = { lang: loadLang() };
|
||
const builderState = { meal:'full', sel:{ protein:[], obstgem:[], fett:[], kh:[] } };
|
||
function t(key){ return UI[state.lang][key]; }
|
||
function tr(obj){ return obj[state.lang]; }
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// render helpers
|
||
// ---------------------------------------------------------------------------
|
||
function dot(f){
|
||
if(f==='pick') return `<span class="dotmark pick">●</span>`;
|
||
if(f==='caution') return `<span class="dotmark caution">●</span>`;
|
||
return '';
|
||
}
|
||
function rowsHtml(items){
|
||
return items.map(it=>`<tr><td>${tr(it.name)}</td><td class="${it.approx?'approx':''}">${dot(it.f)}${tr(it.v)}</td></tr>`).join('');
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// builder helpers
|
||
// ---------------------------------------------------------------------------
|
||
function catItems(id){
|
||
const c = CATS.find(x=>x.id===id);
|
||
return c.sub ? c.items.concat(c.sub.items) : c.items;
|
||
}
|
||
function obstGemItems(){
|
||
return catItems('obst').concat(catItems('gemuese'));
|
||
}
|
||
function findItem(slot, id){
|
||
const list = slot==='protein' ? catItems('protein')
|
||
: slot==='obstgem' ? obstGemItems()
|
||
: slot==='fett' ? catItems('fett')
|
||
: catItems('kh');
|
||
return list.find(it=>it.id===id);
|
||
}
|
||
|
||
function renderHome(){
|
||
document.getElementById('homegrid').innerHTML = `
|
||
<button class="tile teller" onclick="showView('teller')">
|
||
<span class="ic">🍽️</span>
|
||
<div><div class="t">${t('tileTellerT')}</div><div class="d">${t('tileTellerD')}</div></div>
|
||
</button>
|
||
<button class="tile vorrat" onclick="showView('vorrat')">
|
||
<span class="ic">🧺</span>
|
||
<div><div class="t">${t('tileVorratT')}</div><div class="d">${t('tileVorratD')}</div></div>
|
||
</button>
|
||
<button class="tile builder" onclick="showView('builder')">
|
||
<span class="ic">🧩</span>
|
||
<div><div class="t">${t('tileBuilderT')}</div><div class="d">${t('tileBuilderD')}</div></div>
|
||
</button>
|
||
<button class="tile settings" onclick="showView('settings')">
|
||
<span class="ic">⚙️</span>
|
||
<div><div class="t">${t('tileSettingsT')}</div><div class="d">${t('tileSettingsD')}</div></div>
|
||
</button>
|
||
<button class="tile soon" disabled>
|
||
<span class="ic">🍬</span>
|
||
<div><div class="t">${t('tileSugarT')}</div><div class="d">${t('tileSugarD')}</div></div>
|
||
</button>
|
||
<button class="tile soon" disabled>
|
||
<span class="ic">✅</span>
|
||
<div><div class="t">${t('tileCheckT')}</div><div class="d">${t('tileCheckD')}</div></div>
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
function renderSteps(){
|
||
document.getElementById('stepsList').innerHTML = STEPS.map(s=>`
|
||
<div class="acc" id="step-${s.n}">
|
||
<button class="acc-head" onclick="toggleAcc('step-${s.n}')">
|
||
<span class="num ${s.cls}">${s.n}</span>
|
||
<span class="title">${tr(s.title)}</span>
|
||
${tr(s.qty) ? `<span class="qty">${tr(s.qty)}</span>` : ''}
|
||
<span class="chev">›</span>
|
||
</button>
|
||
<div class="acc-body"><p>${tr(s.text)}</p></div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderTellerStatic(){
|
||
document.getElementById('segFull').textContent = t('segFull');
|
||
document.getElementById('segHalf').textContent = t('segHalf');
|
||
document.getElementById('legendFull').innerHTML = `
|
||
<span><i class="dot g"></i>${t('legFullP')}</span>
|
||
<span><i class="dot t"></i>${t('legFullVO')}</span>
|
||
<span><i class="dot o"></i><i class="dot s"></i>${t('legFullFK')}</span>
|
||
`;
|
||
document.getElementById('legendHalf').innerHTML = `
|
||
<span><i class="dot g"></i>${t('legHalfP')}</span>
|
||
<span><i class="dot t"></i>${t('legHalfVO')}</span>
|
||
`;
|
||
document.getElementById('tellerRules').innerHTML = ['rule1','rule2','rule3','rule4']
|
||
.map(k=>`<div class="rule">${t(k)}</div>`).join('');
|
||
}
|
||
|
||
function renderCats(filter){
|
||
const f = (filter||'').trim().toLowerCase();
|
||
const html = CATS.map(c=>{
|
||
let items = c.items;
|
||
let subitems = c.sub ? c.sub.items : null;
|
||
let matchCount = items.length + (subitems?subitems.length:0);
|
||
let forceOpen = false;
|
||
if(f){
|
||
items = c.items.filter(it=>tr(it.name).toLowerCase().includes(f));
|
||
subitems = subitems ? subitems.filter(it=>tr(it.name).toLowerCase().includes(f)) : null;
|
||
matchCount = items.length + (subitems?subitems.length:0);
|
||
forceOpen = matchCount>0;
|
||
if(matchCount===0) return '';
|
||
}
|
||
return `
|
||
<div class="acc ${c.cls} ${forceOpen?'open':''}" id="cat-${c.id}">
|
||
<button class="acc-head cat-head" onclick="toggleAcc('cat-${c.id}')">
|
||
<span class="ic">${c.ic}</span>
|
||
<span class="title">${tr(c.title)}</span>
|
||
<span class="count">${matchCount}</span>
|
||
<span class="chev">›</span>
|
||
</button>
|
||
<div class="acc-body">
|
||
<table>${rowsHtml(items)}</table>
|
||
${subitems && subitems.length ? `<p class="subhead">${tr(c.sub.title)}</p><table>${rowsHtml(subitems)}</table>` : ''}
|
||
${c.recipe ? `<div class="recipe"><b>${tr(c.recipe.title)}</b><br>${tr(c.recipe.text)}</div>` : ''}
|
||
${c.note ? `<p class="subhead" style="text-transform:none;font-weight:400;">${tr(c.note)}</p>` : ''}
|
||
<div class="rot"><b>${c.thumbLabel ? tr(c.thumbLabel) : t('thumbDefault')}</b><br>${tr(c.thumb)}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
document.getElementById('catList').innerHTML = html || `<p style="color:var(--ink-soft); font-size:0.85rem;">${t('noResults')}</p>`;
|
||
}
|
||
|
||
function renderVorratStatic(){
|
||
document.getElementById('searchInput').placeholder = t('searchPlaceholder');
|
||
document.getElementById('unitnote').innerHTML =
|
||
`${t('unitnote')} <span class="dotmark pick">●</span> ${t('pickLabel')} · <span class="dotmark caution">●</span> ${t('cautionLabel')}`;
|
||
}
|
||
|
||
function renderBuilder(){
|
||
document.getElementById('bSegFull').textContent = t('segFull');
|
||
document.getElementById('bSegHalf').textContent = t('segHalf');
|
||
|
||
const slots = builderState.meal==='full'
|
||
? [['protein','pickProtein'],['obstgem','pickObstGem'],['fett','pickFett'],['kh','pickKh']]
|
||
: [['protein','pickProtein'],['obstgem','pickObstGem']];
|
||
|
||
document.getElementById('builderPickers').innerHTML = slots.map(([slot,labelKey])=>{
|
||
const list = slot==='protein' ? catItems('protein')
|
||
: slot==='obstgem' ? obstGemItems()
|
||
: slot==='fett' ? catItems('fett')
|
||
: catItems('kh');
|
||
const chips = list.map(it=>{
|
||
const on = builderState.sel[slot].includes(it.id);
|
||
return `<button class="chip ${on?'on':''}" onclick="toggleBuilderSel('${slot}','${it.id}')">${dot(it.f)}${tr(it.name)}</button>`;
|
||
}).join('');
|
||
const count = builderState.sel[slot].length;
|
||
return `
|
||
<div class="builder-row ${slot}">
|
||
<div class="rowhead">
|
||
<label>${t(labelKey)}</label>
|
||
${count ? `<span class="rcount">${count}×</span>` : ''}
|
||
</div>
|
||
<div class="chip-grid">${chips}</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
evaluateBuilder();
|
||
}
|
||
|
||
function toggleBuilderSel(slot, id){
|
||
const arr = builderState.sel[slot];
|
||
const idx = arr.indexOf(id);
|
||
if(idx===-1) arr.push(id); else arr.splice(idx,1);
|
||
renderBuilder();
|
||
}
|
||
|
||
function setBuilderMeal(which){
|
||
builderState.meal = which;
|
||
document.getElementById('bSegFull').classList.toggle('active', which==='full');
|
||
document.getElementById('bSegHalf').classList.toggle('active', which==='half');
|
||
renderBuilder();
|
||
}
|
||
|
||
function evaluateBuilder(){
|
||
const required = builderState.meal==='full' ? ['protein','obstgem','fett','kh'] : ['protein','obstgem'];
|
||
const missing = required.filter(k=>builderState.sel[k].length===0);
|
||
const box = document.getElementById('builderResult');
|
||
const labelKeys = {protein:'pickProtein', obstgem:'pickObstGem', fett:'pickFett', kh:'pickKh'};
|
||
|
||
if(missing.length){
|
||
box.className = 'builder-result neutral';
|
||
box.innerHTML = `<div class="bh">${t('bResultMissingH')}</div>
|
||
<div>${t('bResultMissingList')} ${missing.map(k=>t(labelKeys[k])).join(', ')}</div>`;
|
||
return;
|
||
}
|
||
|
||
const items = [];
|
||
required.forEach(k=>builderState.sel[k].forEach(id=>items.push({slot:k, item:findItem(k, id)})));
|
||
const cautions = items.filter(x=>x.item.f==='caution');
|
||
const picks = items.filter(x=>x.item.f==='pick');
|
||
const isSnack = builderState.meal==='half';
|
||
|
||
if(cautions.length){
|
||
box.className = 'builder-result caution';
|
||
box.innerHTML = `<div class="bh">🧡 ${t('bResultCautionH')}</div>
|
||
<div>${t('bResultCautionList')}</div>
|
||
<ul>${cautions.map(x=>`<li>${tr(x.item.name)}</li>`).join('')}</ul>
|
||
<div style="margin-top:6px;">${t('bResultCautionNote')}</div>`;
|
||
} else if(picks.length){
|
||
box.className = 'builder-result pick';
|
||
box.innerHTML = `<div class="bh">✅ ${isSnack ? t('bResultPickHSnack') : t('bResultPickH')}</div>
|
||
<div>${t('bResultPickList')}</div>
|
||
<ul>${picks.map(x=>`<li>${tr(x.item.name)}</li>`).join('')}</ul>`;
|
||
} else {
|
||
box.className = 'builder-result solid';
|
||
box.innerHTML = `<div class="bh">👍 ${isSnack ? t('bResultSolidHSnack') : t('bResultSolidH')}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderSettings(){
|
||
document.getElementById('setLangLabel').textContent = t('setLangLabel');
|
||
document.getElementById('setMoreLabel').textContent = t('setMoreLabel');
|
||
document.getElementById('setMoreNote').textContent = t('setMoreNote');
|
||
const note = document.getElementById('persistNote');
|
||
note.textContent = persistWorks ? t('persistOk') : t('persistWarn');
|
||
note.className = 'setnote ' + (persistWorks ? 'ok' : 'warn');
|
||
}
|
||
|
||
function renderLangPills(){
|
||
const de = state.lang==='de';
|
||
['pillDe','pillDe2'].forEach(id=>document.getElementById(id).classList.toggle('active', de));
|
||
['pillEn','pillEn2'].forEach(id=>document.getElementById(id).classList.toggle('active', !de));
|
||
}
|
||
|
||
function renderAll(){
|
||
renderLangPills();
|
||
renderHome();
|
||
renderSteps();
|
||
renderTellerStatic();
|
||
renderVorratStatic();
|
||
renderCats(document.getElementById('searchInput').value);
|
||
renderBuilder();
|
||
renderSettings();
|
||
refreshTopTitle();
|
||
}
|
||
|
||
function setLang(l){
|
||
if(l===state.lang) return;
|
||
state.lang = l;
|
||
saveLang(l);
|
||
renderAll();
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// navigation
|
||
// ---------------------------------------------------------------------------
|
||
function toggleAcc(id){
|
||
document.getElementById(id).classList.toggle('open');
|
||
}
|
||
|
||
function setMeal(which){
|
||
const isFull = which==='full';
|
||
document.getElementById('segFull').classList.toggle('active', isFull);
|
||
document.getElementById('segHalf').classList.toggle('active', !isFull);
|
||
document.getElementById('plateFull').style.display = isFull?'block':'none';
|
||
document.getElementById('plateHalf').style.display = isFull?'none':'block';
|
||
document.getElementById('legendFull').style.display = isFull?'flex':'none';
|
||
document.getElementById('legendHalf').style.display = isFull?'none':'flex';
|
||
}
|
||
|
||
let currentView = 'home';
|
||
function refreshTopTitle(){
|
||
const back = document.getElementById('backBtn');
|
||
const eyebrow = document.getElementById('topEyebrow');
|
||
const title = document.getElementById('topTitle');
|
||
if(currentView==='home'){
|
||
back.classList.remove('show');
|
||
eyebrow.textContent = t('body');
|
||
title.textContent = t('appTitle');
|
||
} else {
|
||
back.classList.add('show');
|
||
if(currentView==='teller'){ eyebrow.textContent = t('card1'); title.textContent = t('tileTellerT'); }
|
||
else if(currentView==='vorrat'){ eyebrow.textContent = t('card2'); title.textContent = t('tileVorratT'); }
|
||
else if(currentView==='builder'){ eyebrow.textContent = t('appTitle'); title.textContent = t('cardBuilder'); }
|
||
else if(currentView==='settings'){ eyebrow.textContent = t('appTitle'); title.textContent = t('cardSettings'); }
|
||
}
|
||
}
|
||
|
||
function showView(name){
|
||
currentView = name;
|
||
document.querySelectorAll('.view').forEach(v=>v.classList.remove('active'));
|
||
document.getElementById('view-'+name).classList.add('active');
|
||
refreshTopTitle();
|
||
window.scrollTo(0,0);
|
||
history.pushState({view:name}, '', '#'+name);
|
||
}
|
||
function goHome(){ showView('home'); }
|
||
|
||
function onSearch(){
|
||
const val = document.getElementById('searchInput').value;
|
||
document.getElementById('clearBtn').style.display = val ? 'block' : 'none';
|
||
renderCats(val);
|
||
}
|
||
function clearSearch(){
|
||
document.getElementById('searchInput').value='';
|
||
document.getElementById('clearBtn').style.display='none';
|
||
renderCats('');
|
||
}
|
||
|
||
window.addEventListener('popstate', (e)=>{
|
||
const v = (e.state && e.state.view) || 'home';
|
||
currentView = v;
|
||
document.querySelectorAll('.view').forEach(el=>el.classList.remove('active'));
|
||
document.getElementById('view-'+v).classList.add('active');
|
||
refreshTopTitle();
|
||
});
|
||
|
||
renderAll();
|
||
</script>
|