Release: one log page, grouped dishes, live search #1

Merged
Kessinen merged 10 commits from dev into main 2026-09-05 19:28:54 +00:00
2 changed files with 109 additions and 53 deletions
Showing only changes of commit 47615ddabe - Show all commits
+39 -2
View File
@@ -423,15 +423,52 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; }
cursor: pointer;
}
/* Catalog rows */
/* Catalog structure: Pääruuat and Lisukkeet are the two halves of the
catalog, the categories are subdivisions of the first. Two levels, so they
must not look alike. */
.section + .section { margin-top: 34px; }
.sectiontitle {
display: flex;
align-items: center;
gap: 9px;
margin: 0 0 4px;
padding-bottom: 8px;
border-bottom: 2px solid var(--ink);
font-size: 18px;
font-weight: 700;
letter-spacing: -0.03em;
}
.sectiontitle .count {
padding: 2px 8px;
border-radius: 999px;
background: var(--sunk);
color: var(--muted);
font-size: 12px;
font-weight: 600;
letter-spacing: 0;
}
.sechead {
margin: 26px 0 6px;
margin: 20px 0 2px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--muted);
}
/* Collapsed add/edit forms, so the page opens on the catalog. */
.addform > summary {
cursor: pointer;
font-weight: 600;
font-size: 15px;
min-height: 24px;
}
.addform[open] > summary {
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1px solid var(--line);
}
.row {
display: flex;
align-items: center;
+70 -51
View File
@@ -531,8 +531,6 @@ templ catalogPage(v catalogView) {
if v.Report != nil {
@importReport(v.Report)
}
@mainForm_(v.Main)
@sideForm_(v.Side)
<div data-signals:haku={ jsString(v.Search) }>
<form method="get" action="/ruuat" class="searchrow">
<input
@@ -546,6 +544,10 @@ templ catalogPage(v catalogView) {
data-on:input__debounce.250ms="@get('/ruuat/etsi')"
/>
</form>
// The add and edit forms stay outside the patched fragment, or
// typing in the search box would collapse a form mid-edit.
@mainForm_(v.Main)
@sideForm_(v.Side)
@catalogList(v)
</div>
<details class="card">
@@ -557,55 +559,69 @@ templ catalogPage(v catalogView) {
}
// catalogList carries the id Datastar patches, so typing in the search box
// swaps the lists without reloading the forms above them.
// swaps the lists without touching the forms above them.
//
// Two levels of heading, because there are two: Pääruuat and Lisukkeet are
// the halves of the catalog, and the categories are subdivisions of the
// first. They were previously styled the same, which made a category look
// like a peer of the entire side-dish list.
templ catalogList(v catalogView) {
<div id="ruokalista">
if v.Mains == 0 {
<h3 class="sechead">Pääruuat</h3>
<p class="muted small">
if v.Search == "" {
Ei vielä pääruokia.
} else {
Ei osumia.
}
</p>
}
// Grouped by category, alphabetical inside. The catalog is a list you
// manage, so a predictable position beats a useful one.
for _, g := range v.Groups {
<h3 class="sechead">{ g.Label }</h3>
for _, d := range g.Dishes {
<div class="row">
@categoryIcon(d.CategoryKey())
<div class="rowtext">
<div class="nm">{ d.Name }</div>
if !d.HasSides {
<div class="sd">Ei lisukkeita</div>
}
<section class="section">
@sectionTitle("Pääruuat", v.Mains)
if v.Mains == 0 {
@emptyNote(v.Search)
}
// Grouped by category, alphabetical inside. The catalog is a list
// you manage, so a predictable position beats a useful one.
for _, g := range v.Groups {
<h3 class="sechead">{ g.Label }</h3>
for _, d := range g.Dishes {
<div class="row">
@categoryIcon(d.CategoryKey())
<div class="rowtext">
<div class="nm">{ d.Name }</div>
if !d.HasSides {
<div class="sd">Ei lisukkeita</div>
}
</div>
@rowActions(v, "/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa")
</div>
@rowActions(v, "/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa")
}
}
</section>
<section class="section">
@sectionTitle("Lisukkeet", len(v.Sides))
if len(v.Sides) == 0 {
@emptyNote(v.Search)
}
for _, s := range v.Sides {
<div class="row">
<div class="rowtext"><div class="nm">{ s.Name }</div></div>
@rowActions(v, "/ruuat?muokkaa-lisuke="+strconv.FormatInt(s.ID, 10), s.ID, "lisuke")
</div>
}
}
<h3 class="sechead">Lisukkeet</h3>
if len(v.Sides) == 0 {
<p class="muted small">
if v.Search == "" {
Ei vielä lisukkeita.
} else {
Ei osumia.
}
</p>
}
for _, s := range v.Sides {
<div class="row">
<div class="rowtext"><div class="nm">{ s.Name }</div></div>
@rowActions(v, "/ruuat?muokkaa-lisuke="+strconv.FormatInt(s.ID, 10), s.ID, "lisuke")
</div>
}
</section>
</div>
}
templ sectionTitle(label string, n int) {
<h2 class="sectiontitle">
{ label }
<span class="count">{ strconv.Itoa(n) }</span>
</h2>
}
templ emptyNote(search string) {
<p class="muted small">
if search == "" {
Ei vielä mitään.
} else {
Ei osumia haulle { search }.
}
</p>
}
// rowActions is a pencil and a bin, until the bin is tapped: then the row
// asks. An icon is a smaller target to hit by accident than a word, and the
// dish disappears from every picker the moment it goes.
@@ -651,15 +667,18 @@ templ iconTrash() {
</svg>
}
// Collapsed by default so the page opens on the catalog rather than on two
// screens of empty form. Forced open when editing or after a rejected
// submission, since the form is then the thing that needs attention.
templ mainForm_(f mainForm) {
<section class="card" id="paaruoka">
<h3>
<details class="card addform" id="paaruoka" open?={ f.ID != 0 || f.Err != "" }>
<summary>
if f.ID == 0 {
Lisää pääruoka
} else {
Muokkaa pääruokaa
}
</h3>
</summary>
if f.Err != "" {
<p class="formerr">{ f.Err }</p>
}
@@ -693,7 +712,7 @@ templ mainForm_(f mainForm) {
if f.ID != 0 {
<a class="ghost" href="/ruuat">Peruuta</a>
}
</section>
</details>
}
templ categoryChip(value, label string, f mainForm) {
@@ -709,14 +728,14 @@ templ categoryChip(value, label string, f mainForm) {
}
templ sideForm_(f sideForm) {
<section class="card" id="lisuke">
<h3>
<details class="card addform" id="lisuke" open?={ f.ID != 0 || f.Err != "" }>
<summary>
if f.ID == 0 {
Lisää lisuke
} else {
Muokkaa lisuketta
}
</h3>
</summary>
if f.Err != "" {
<p class="formerr">{ f.Err }</p>
}
@@ -733,7 +752,7 @@ templ sideForm_(f sideForm) {
if f.ID != 0 {
<a class="ghost" href="/ruuat">Peruuta</a>
}
</section>
</details>
}
templ importForm() {