Foodster is a self-hosted dinner log and meal suggester for one household. Stage 1 (eating history) is in development; stage 2 (the suggester) follows once there is enough history to weight against. Replace the PRD's original stack (Nuxt, Postgres, Drizzle, Pico CSS) with Go 1.27, net/http, templ, Datastar and SQLite — one static binary, no Node.js in the build. Sections 3, 4, 5, 9, 10 and 11 are rewritten to match. Record the decisions made while reviewing mockups: - the UI is written in Finnish; PRD, code and comments stay English - access is one shared password over HTTP Basic rather than open on the LAN - images are CalVer vYYYYMMDD-N, built with Podman, run under Docker Compose - registry coordinates live in .env, so no infrastructure detail is committed and the MIT publication option stays open mockups/ holds standalone HTML design studies. log-fi.html is the current one; the others are superseded exploration kept for reference.
200 lines
8.8 KiB
HTML
200 lines
8.8 KiB
HTML
<!doctype html>
|
|
<html lang="en" data-theme="auto">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Foodster — C · Fridge board</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Kalam:wght@400;700&family=Figtree:wght@400;500;700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{
|
|
color-scheme: light dark;
|
|
--board: light-dark(#E3E7E3, #1A1D1B);
|
|
--card: light-dark(#FCFCFA, #24272A);
|
|
--ink: light-dark(#1D2724, #E6E9E5);
|
|
--muted: light-dark(#6F7A75, #8E9994);
|
|
--line: light-dark(#D2D8D3, #33383A);
|
|
--marker:light-dark(#16403C, #6FD3C4);
|
|
--tape: light-dark(#E8D48A, #6E6134);
|
|
--meat: light-dark(#AF4230, #E07561);
|
|
--chicken: light-dark(#BE8410, #DFA83B);
|
|
--fish: light-dark(#25688F, #63AAD8);
|
|
--vegetarian:light-dark(#457A3C, #82BE7A);
|
|
}
|
|
html[data-theme="light"]{ color-scheme: only light; }
|
|
html[data-theme="dark"] { color-scheme: only dark; }
|
|
|
|
*{box-sizing:border-box;}
|
|
body{margin:0; background:var(--board); color:var(--ink);
|
|
font-family:Figtree,system-ui,sans-serif; font-size:16px; line-height:1.5;
|
|
-webkit-font-smoothing:antialiased;}
|
|
.wrap{max-width:1100px; margin:0 auto; padding:0 20px 80px;}
|
|
|
|
.top{display:flex; align-items:center; gap:18px; flex-wrap:wrap; padding:22px 0 0;}
|
|
.logo{font-family:Kalam,cursive; font-weight:700; font-size:26px; color:var(--marker);}
|
|
nav{display:flex; gap:6px; margin-left:auto;}
|
|
nav a{font-size:14px; font-weight:500; color:var(--muted); text-decoration:none; padding:6px 2px;
|
|
border-bottom:2px solid transparent;}
|
|
nav a[aria-current]{color:var(--ink); border-color:var(--marker);}
|
|
nav a:hover{color:var(--ink);}
|
|
.themebtn{font-family:Figtree,sans-serif; font-size:13px; background:none;
|
|
border:1px solid var(--line); color:var(--muted); padding:6px 12px;
|
|
border-radius:6px; cursor:pointer;}
|
|
.themebtn:hover{color:var(--ink);}
|
|
|
|
header.lede{padding:38px 0 8px; max-width:46ch;}
|
|
h1{font-family:Figtree,sans-serif; font-weight:800; font-size:clamp(34px,5.5vw,52px);
|
|
line-height:1.05; letter-spacing:-.03em; margin:0 0 10px;}
|
|
.lede p{margin:0; color:var(--muted);}
|
|
.handnote{font-family:Kalam,cursive; font-size:19px; color:var(--marker);
|
|
transform:rotate(-1.1deg); margin:18px 0 0;}
|
|
|
|
.actions{display:flex; gap:10px; margin:24px 0 26px;}
|
|
.regen{font-family:Figtree,sans-serif; font-weight:700; font-size:15px;
|
|
background:var(--marker); color:var(--board); border:0; padding:12px 22px;
|
|
border-radius:7px; cursor:pointer;}
|
|
.regen:hover{filter:brightness(1.12);}
|
|
|
|
/* ---- cards on the board ---- */
|
|
.cards{display:grid; gap:22px; grid-template-columns:repeat(auto-fill,minmax(238px,1fr));}
|
|
.card{position:relative; background:var(--card); border-radius:3px; padding:26px 18px 16px;
|
|
box-shadow:0 1px 2px rgba(0,0,0,.10), 0 6px 14px -8px rgba(0,0,0,.22);}
|
|
.card:nth-child(3n+1){transform:rotate(-.55deg);}
|
|
.card:nth-child(3n+2){transform:rotate(.4deg);}
|
|
.card:nth-child(3n+3){transform:rotate(-.2deg);}
|
|
.card::before{content:""; position:absolute; top:-7px; left:50%; margin-left:-7px;
|
|
width:14px; height:14px; border-radius:50%; background:var(--mag,var(--muted));
|
|
box-shadow:inset 0 -2px 3px rgba(0,0,0,.28);}
|
|
.m-meat{--mag:var(--meat);} .m-chicken{--mag:var(--chicken);}
|
|
.m-fish{--mag:var(--fish);} .m-vegetarian{--mag:var(--vegetarian);}
|
|
.card.wild::before{background:conic-gradient(var(--meat) 0 25%, var(--chicken) 25% 50%,
|
|
var(--fish) 50% 75%, var(--vegetarian) 75% 100%);}
|
|
|
|
.tag{font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
|
|
color:var(--muted); margin:0;}
|
|
.dish{font-family:Figtree,sans-serif; font-weight:700; font-size:25px; line-height:1.08;
|
|
letter-spacing:-.025em; margin:8px 0 8px;}
|
|
.sides{margin:0 0 10px; font-size:14px; color:var(--ink);}
|
|
.sides.none{color:var(--muted); font-style:italic;}
|
|
.last{font-size:12.5px; color:var(--muted); margin:0 0 14px;}
|
|
.swaps{display:flex; gap:6px; border-top:1px solid var(--line); padding-top:12px;}
|
|
.swaps button{flex:1; font-family:Figtree,sans-serif; font-size:12.5px; font-weight:600;
|
|
background:none; color:var(--muted); border:1px solid var(--line);
|
|
border-radius:6px; padding:8px 5px; cursor:pointer;}
|
|
.swaps button:hover:not(:disabled){color:var(--marker); border-color:var(--marker);}
|
|
.swaps button:disabled{opacity:.4; cursor:not-allowed;}
|
|
|
|
/* ---- the split: handwriting = what we actually ate ---- */
|
|
.logstrip{margin-top:56px; border-top:2px dashed var(--line); padding-top:24px;}
|
|
.logstrip h2{font-family:Figtree,sans-serif; font-weight:800; font-size:18px;
|
|
letter-spacing:-.02em; margin:0 0 4px;}
|
|
.logstrip .sub{color:var(--muted); font-size:14px; margin:0 0 18px;}
|
|
.logrow{display:flex; gap:14px; align-items:baseline; padding:10px 0;
|
|
border-bottom:1px solid var(--line);}
|
|
.logrow time{font-size:13px; color:var(--muted); min-width:80px;}
|
|
.logrow .hand{font-family:Kalam,cursive; font-size:22px; color:var(--marker); line-height:1.2;}
|
|
.logrow em{font-family:Kalam,cursive; font-style:normal; font-size:17px;
|
|
color:var(--muted); margin-left:auto;}
|
|
|
|
:focus-visible{outline:2.5px solid var(--marker); outline-offset:3px;}
|
|
@media (max-width:560px){ .card{transform:none!important;} }
|
|
@media (prefers-reduced-motion:reduce){ *{transition:none!important;} }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
|
|
<div class="top">
|
|
<div class="logo">Foodster</div>
|
|
<nav><a href="#" aria-current="page">Plan</a><a href="#">Log</a><a href="#">Catalog</a></nav>
|
|
<button class="themebtn" id="t">Theme: auto</button>
|
|
</div>
|
|
|
|
<header class="lede">
|
|
<h1>Seven dinners on the door.</h1>
|
|
<p>Take them in any order. Swap what nobody's in the mood for.</p>
|
|
<p class="handnote">— last regenerated Sunday</p>
|
|
</header>
|
|
|
|
<div class="actions"><button class="regen">New seven</button></div>
|
|
|
|
<div class="cards">
|
|
|
|
<article class="card m-meat">
|
|
<p class="tag">Meat</p>
|
|
<h3 class="dish">Jauhelihakastike</h3>
|
|
<p class="sides">Perunamuusi · Vihersalaatti</p>
|
|
<p class="last">Last eaten 23 days ago</p>
|
|
<div class="swaps"><button>Swap similar</button><button>Swap anything</button></div>
|
|
</article>
|
|
|
|
<article class="card m-chicken">
|
|
<p class="tag">Chicken</p>
|
|
<h3 class="dish">Broilerikastike</h3>
|
|
<p class="sides">Riisi</p>
|
|
<p class="last">Last eaten 31 days ago</p>
|
|
<div class="swaps"><button>Swap similar</button><button>Swap anything</button></div>
|
|
</article>
|
|
|
|
<article class="card wild">
|
|
<p class="tag">Meat · Chicken · Fish · Veg</p>
|
|
<h3 class="dish">Tortillat</h3>
|
|
<p class="sides none">Served on its own</p>
|
|
<p class="last">Last eaten 40 days ago</p>
|
|
<div class="swaps">
|
|
<button disabled title="Nothing else covers all four">Swap similar</button>
|
|
<button>Swap anything</button>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="card m-fish">
|
|
<p class="tag">Fish</p>
|
|
<h3 class="dish">Uunilohi</h3>
|
|
<p class="sides">Riisi · Vihersalaatti</p>
|
|
<p class="last">Last eaten 34 days ago</p>
|
|
<div class="swaps"><button>Swap similar</button><button>Swap anything</button></div>
|
|
</article>
|
|
|
|
<article class="card m-vegetarian">
|
|
<p class="tag">Vegetarian</p>
|
|
<h3 class="dish">Kasvispyörykät</h3>
|
|
<p class="sides">Perunamuusi · Höyrytetyt Porkkanat</p>
|
|
<p class="last">Last eaten 52 days ago</p>
|
|
<div class="swaps"><button>Swap similar</button><button>Swap anything</button></div>
|
|
</article>
|
|
|
|
<article class="card m-meat">
|
|
<p class="tag">Meat</p>
|
|
<h3 class="dish">Makaronilaatikko</h3>
|
|
<p class="sides none">Served on its own</p>
|
|
<p class="last">Last eaten 19 days ago</p>
|
|
<div class="swaps"><button>Swap similar</button><button>Swap anything</button></div>
|
|
</article>
|
|
|
|
<article class="card m-vegetarian">
|
|
<p class="tag">Vegetarian</p>
|
|
<h3 class="dish">Hernekeitto</h3>
|
|
<p class="sides none">Served on its own</p>
|
|
<p class="last">Last eaten 27 days ago</p>
|
|
<div class="swaps"><button>Swap similar</button><button>Swap anything</button></div>
|
|
</article>
|
|
|
|
</div>
|
|
|
|
<section class="logstrip">
|
|
<h2>What we actually ate</h2>
|
|
<p class="sub">Printed above is the app guessing. Below is the house, on the record.</p>
|
|
<div class="logrow"><time>Thu 3 Sep</time><span class="hand">Lohikeitto</span><em>ruisleipä</em></div>
|
|
<div class="logrow"><time>Wed 2 Sep</time><span class="hand">Kanacurry</span><em>riisi</em></div>
|
|
<div class="logrow"><time>Tue 1 Sep</time><span class="hand">Uunimakkara</span><em>perunamuusi, vihersalaatti</em></div>
|
|
</section>
|
|
|
|
</div>
|
|
<script>
|
|
const t=document.getElementById('t'), h=document.documentElement, m=['auto','light','dark'];
|
|
t.onclick=()=>{const n=m[(m.indexOf(h.dataset.theme)+1)%3]; h.dataset.theme=n; t.textContent='Theme: '+n;};
|
|
</script>
|
|
</body>
|
|
</html>
|