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
4 changed files with 131 additions and 26 deletions
Showing only changes of commit b67c4edd5a - Show all commits
+17
View File
@@ -72,6 +72,10 @@ type logView struct {
Sides []Side Sides []Side
New mainForm // inline "add the dish you were looking for" New mainForm // inline "add the dish you were looking for"
History []HistoryRow History []HistoryRow
// Deleting a logged meal drops the row outright, unlike a dish which is
// only soft-deleted, so it asks first.
Confirming bool
} }
func (a *app) index(w http.ResponseWriter, r *http.Request) { func (a *app) index(w http.ResponseWriter, r *http.Request) {
@@ -83,6 +87,8 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) {
Checked: map[int64]bool{}, Checked: map[int64]bool{},
} }
v.Confirming = r.URL.Query().Get("poista") != ""
entry, err := entryFor(a.db, date) entry, err := entryFor(a.db, date)
if err != nil { if err != nil {
log.Printf("entry for %s: %v", date.Format(dateLayout), err) log.Printf("entry for %s: %v", date.Format(dateLayout), err)
@@ -270,6 +276,11 @@ type catalogView struct {
Side sideForm Side sideForm
Report *ImportReport Report *ImportReport
Mains int // count, for the header Mains int // count, for the header
// The row awaiting a delete confirmation, if any. A trash icon is easy to
// hit by accident, so the row asks before anything happens.
DeleteID int64
DeleteKind string
} }
func (a *app) catalog(w http.ResponseWriter, r *http.Request) { func (a *app) catalog(w http.ResponseWriter, r *http.Request) {
@@ -300,6 +311,12 @@ func (a *app) catalog(w http.ResponseWriter, r *http.Request) {
} }
} }
} }
if raw := r.URL.Query().Get("poista"); raw != "" {
if id, err := strconv.ParseInt(raw, 10, 64); err == nil {
v.DeleteID = id
v.DeleteKind = r.URL.Query().Get("tyyppi")
}
}
a.renderCatalog(w, r, v) a.renderCatalog(w, r, v)
} }
+14 -2
View File
@@ -427,12 +427,14 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; }
.rowtext { flex: 1; min-width: 0; } .rowtext { flex: 1; min-width: 0; }
.rowtext .nm { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; } .rowtext .nm { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; }
.rowtext .sd { font-size: 12.5px; color: var(--muted); } .rowtext .sd { font-size: 12.5px; color: var(--muted); }
.rowactions { display: flex; align-items: center; gap: 4px; flex: none; } .rowactions { display: flex; align-items: center; gap: 2px; flex: none; }
.rowactions a, .rowactions button { .rowactions a, .rowactions button {
min-width: 40px;
min-height: 40px; min-height: 40px;
padding: 0 10px; padding: 0 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
background: none; background: none;
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
@@ -442,9 +444,19 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; }
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
.rowactions svg { display: block; }
.rowactions a:hover { color: var(--accent); } .rowactions a:hover { color: var(--accent); }
.rowactions .del { color: var(--liha); } .rowactions .del { color: var(--liha); }
/* The row asks before a delete happens; an icon is easy to hit by accident. */
.rowactions.confirming {
gap: 4px;
font-size: 13px;
color: var(--muted);
}
.rowactions.confirming > span { padding-right: 2px; }
.rowactions.confirming .del { color: var(--liha); font-weight: 700; }
.field input[type="text"] { .field input[type="text"] {
width: 100%; width: 100%;
min-height: var(--tap); min-height: var(--tap);
+55 -11
View File
@@ -360,20 +360,30 @@ templ loggedCard(v logView) {
{ v.Entry.Main.Name } { v.Entry.Main.Name }
</p> </p>
<p class="sd">{ v.Entry.SidesLabel() }</p> <p class="sd">{ v.Entry.SidesLabel() }</p>
if v.Confirming {
<p class="q">Poistetaanko merkintä?</p>
<div class="pair">
<form method="post" action="/poista">
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
<button class="btn del" type="submit">Kyllä, poista</button>
</form>
<a class="btn" href={ templ.SafeURL(dayURL("/", v.Date, v.Today)) }>Peruuta</a>
</div>
} else {
<div class="pair"> <div class="pair">
<a <a
class="btn" class="btn"
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") } href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
>Muokkaa</a> >Muokkaa</a>
<form method="post" action="/poista"> <a
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/> class="btn del"
<button class="btn del" type="submit">Poista</button> href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "poista=1") }
</form> >Poista</a>
</div> </div>
}
</section> </section>
} }
// ---------------------------------------------------------------- Historia
// ---------------------------------------------------------------- Ruuat // ---------------------------------------------------------------- Ruuat
templ catalogPage(v catalogView) { templ catalogPage(v catalogView) {
@page("Ruuat — Foodster", "/ruuat") { @page("Ruuat — Foodster", "/ruuat") {
@@ -405,7 +415,7 @@ templ catalogPage(v catalogView) {
<div class="sd">Ei lisukkeita</div> <div class="sd">Ei lisukkeita</div>
} }
</div> </div>
@rowActions("/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa") @rowActions(v, "/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa")
</div> </div>
} }
} }
@@ -417,7 +427,7 @@ templ catalogPage(v catalogView) {
for _, s := range v.Sides { for _, s := range v.Sides {
<div class="row"> <div class="row">
<div class="rowtext"><div class="nm">{ s.Name }</div></div> <div class="rowtext"><div class="nm">{ s.Name }</div></div>
@rowActions("/ruuat?muokkaa-lisuke="+strconv.FormatInt(s.ID, 10), s.ID, "lisuke") @rowActions(v, "/ruuat?muokkaa-lisuke="+strconv.FormatInt(s.ID, 10), s.ID, "lisuke")
</div> </div>
} }
<details class="card"> <details class="card">
@@ -428,15 +438,49 @@ templ catalogPage(v catalogView) {
} }
} }
templ rowActions(editURL string, id int64, kind string) { // rowActions is a pencil and a bin, until the bin is tapped: then the row
<div class="rowactions"> // asks. An icon is a smaller target to hit by accident than a word, and the
<a href={ templ.SafeURL(editURL) } aria-label="Muokkaa">Muokkaa</a> // dish disappears from every picker the moment it goes.
templ rowActions(v catalogView, editURL string, id int64, kind string) {
if v.DeleteID == id && v.DeleteKind == kind {
<div class="rowactions confirming">
<span>Poista?</span>
<form method="post" action="/ruuat/poista"> <form method="post" action="/ruuat/poista">
<input type="hidden" name="id" value={ strconv.FormatInt(id, 10) }/> <input type="hidden" name="id" value={ strconv.FormatInt(id, 10) }/>
<input type="hidden" name="tyyppi" value={ kind }/> <input type="hidden" name="tyyppi" value={ kind }/>
<button type="submit" class="del" aria-label="Poista">Poista</button> <button type="submit" class="del">Kyllä</button>
</form> </form>
<a href="/ruuat">Peruuta</a>
</div> </div>
} else {
<div class="rowactions">
<a href={ templ.SafeURL(editURL) } aria-label="Muokkaa" title="Muokkaa">
@iconPencil()
</a>
<a
class="del"
href={ templ.SafeURL("/ruuat?poista=" + strconv.FormatInt(id, 10) + "&tyyppi=" + kind) }
aria-label="Poista"
title="Poista"
>
@iconTrash()
</a>
</div>
}
}
templ iconPencil() {
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M11.1 1.6a1.7 1.7 0 0 1 2.4 0l0.9 0.9a1.7 1.7 0 0 1 0 2.4l-0.8 0.8-3.3-3.3z"></path>
<path d="M9.4 3.3l3.3 3.3-6.6 6.6-4 0.7 0.7-4z"></path>
</svg>
}
templ iconTrash() {
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M6.2 1.3h3.6a1 1 0 0 1 1 1v0.6h3.1v1.7H2.1V2.9h3.1v-.6a1 1 0 0 1 1-1z"></path>
<path d="M3.3 6.2h9.4l-0.7 7.3a1.5 1.5 0 0 1-1.5 1.3H5.5a1.5 1.5 0 0 1-1.5-1.3z"></path>
</svg>
} }
templ mainForm_(f mainForm) { templ mainForm_(f mainForm) {
+34 -2
View File
@@ -43,6 +43,16 @@ check() {
fi fi
} }
# refute <name> <haystack> <needle>
refute() {
if printf '%s' "$2" | grep -qF -- "$3"; then
echo " FAIL $1 (should not contain: $3)"
fail=1
else
echo " ok $1"
fi
}
echo "smoke: http://$addr" echo "smoke: http://$addr"
check "unauthenticated request is refused" \ check "unauthenticated request is refused" \
@@ -125,6 +135,11 @@ check "the saved day shows what was eaten" \
check "history is on the same page as the logger" \ check "history is on the same page as the logger" \
"$(curl -s -u ":$pass" "http://$addr/")" "Aiemmin" "$(curl -s -u ":$pass" "http://$addr/")" "Aiemmin"
# Deleting a logged meal drops the row outright, so it asks first.
saved=$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05&poista=1")
check "deleting a meal asks first" "$saved" "Poistetaanko merkintä?"
refute "and does not delete while asking" "$saved" "Ei merkintää"
check "deleting redirects back" \ check "deleting redirects back" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \ "$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
-d "pvm=2026-09-05" "http://$addr/poista")" "303" -d "pvm=2026-09-05" "http://$addr/poista")" "303"
@@ -196,14 +211,31 @@ check "adding a side redirects" \
check "the new side is listed" \ check "the new side is listed" \
"$(curl -s -u ":$pass" "http://$addr/ruuat")" "Lohkoperunat" "$(curl -s -u ":$pass" "http://$addr/ruuat")" "Lohkoperunat"
uusi=$(printf '%s' "$catalog" | grep -o 'muokkaa=[0-9]*' | head -n1 | cut -d= -f2) # The id of Uunikala specifically: the catalog is grouped and alphabetical, so
# the first id on the page belongs to some other dish entirely.
uusi=$(printf '%s' "$catalog" | grep -o 'Uunikala.*' | grep -o 'muokkaa=[0-9]*' | head -n1 | cut -d= -f2)
if [ -z "$uusi" ]; then
echo " FAIL could not find Uunikala's id in the catalog"
fail=1
uusi=0
fi
check "the edit form is prefilled" \ check "the edit form is prefilled" \
"$(curl -s -u ":$pass" "http://$addr/ruuat?muokkaa=$uusi")" "Muokkaa pääruokaa" "$(curl -s -u ":$pass" "http://$addr/ruuat?muokkaa=$uusi")" "Muokkaa pääruokaa"
check "deleting a main redirects" \ # A bin icon is easy to hit by accident, so the row asks before anything goes.
check "the bin asks before deleting" \
"$(curl -s -u ":$pass" "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Poista?"
check "the dish is still there while it asks" \
"$(curl -s -u ":$pass" "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Uunikala"
check "confirming the delete redirects" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \ "$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
-d "id=$uusi&tyyppi=paa" "http://$addr/ruuat/poista")" "303" -d "id=$uusi&tyyppi=paa" "http://$addr/ruuat/poista")" "303"
refute "the dish is gone once confirmed" \
"$(curl -s -u ":$pass" "http://$addr/ruuat")" "Uunikala"
if [ "$fail" -ne 0 ]; then if [ "$fail" -ne 0 ]; then
echo "smoke: FAILED" echo "smoke: FAILED"
exit 1 exit 1