diff --git a/cmd/foodster/handlers.go b/cmd/foodster/handlers.go index 767b974..2d49eca 100644 --- a/cmd/foodster/handlers.go +++ b/cmd/foodster/handlers.go @@ -72,6 +72,10 @@ type logView struct { Sides []Side New mainForm // inline "add the dish you were looking for" 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) { @@ -83,6 +87,8 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) { Checked: map[int64]bool{}, } + v.Confirming = r.URL.Query().Get("poista") != "" + entry, err := entryFor(a.db, date) if err != nil { log.Printf("entry for %s: %v", date.Format(dateLayout), err) @@ -270,6 +276,11 @@ type catalogView struct { Side sideForm Report *ImportReport 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) { @@ -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) } diff --git a/cmd/foodster/static/app.css b/cmd/foodster/static/app.css index f563b63..794a866 100644 --- a/cmd/foodster/static/app.css +++ b/cmd/foodster/static/app.css @@ -427,12 +427,14 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; } .rowtext { flex: 1; min-width: 0; } .rowtext .nm { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; } .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 { + min-width: 40px; min-height: 40px; - padding: 0 10px; + padding: 0 8px; display: flex; align-items: center; + justify-content: center; background: none; border: 0; border-radius: 8px; @@ -442,9 +444,19 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; } text-decoration: none; cursor: pointer; } +.rowactions svg { display: block; } .rowactions a:hover { color: var(--accent); } .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"] { width: 100%; min-height: var(--tap); diff --git a/cmd/foodster/views.templ b/cmd/foodster/views.templ index 7ec0db5..5944fde 100644 --- a/cmd/foodster/views.templ +++ b/cmd/foodster/views.templ @@ -360,20 +360,30 @@ templ loggedCard(v logView) { { v.Entry.Main.Name }

{ v.Entry.SidesLabel() }

-
- Muokkaa -
- - -
-
+ if v.Confirming { +

Poistetaanko merkintä?

+
+
+ + +
+ Peruuta +
+ } else { +
+ Muokkaa + Poista +
+ } } -// ---------------------------------------------------------------- Historia // ---------------------------------------------------------------- Ruuat templ catalogPage(v catalogView) { @page("Ruuat — Foodster", "/ruuat") { @@ -405,7 +415,7 @@ templ catalogPage(v catalogView) {
Ei lisukkeita
} - @rowActions("/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa") + @rowActions(v, "/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa") } } @@ -417,7 +427,7 @@ templ catalogPage(v catalogView) { for _, s := range v.Sides {
{ s.Name }
- @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")
}
@@ -428,15 +438,49 @@ templ catalogPage(v catalogView) { } } -templ rowActions(editURL string, id int64, kind string) { -
- Muokkaa -
- - - -
-
+// 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. +templ rowActions(v catalogView, editURL string, id int64, kind string) { + if v.DeleteID == id && v.DeleteKind == kind { +
+ Poista? +
+ + + +
+ Peruuta +
+ } else { +
+ + @iconPencil() + + + @iconTrash() + +
+ } +} + +templ iconPencil() { + +} + +templ iconTrash() { + } templ mainForm_(f mainForm) { diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 336c87e..721feef 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -43,6 +43,16 @@ check() { fi } +# refute +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" 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" \ "$(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" \ "$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \ -d "pvm=2026-09-05" "http://$addr/poista")" "303" @@ -196,14 +211,31 @@ check "adding a side redirects" \ check "the new side is listed" \ "$(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" \ "$(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" \ -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 echo "smoke: FAILED" exit 1