Release: one log page, grouped dishes, live search #1
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+66
-22
@@ -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>
|
||||||
<div class="pair">
|
if v.Confirming {
|
||||||
<a
|
<p class="q">Poistetaanko merkintä?</p>
|
||||||
class="btn"
|
<div class="pair">
|
||||||
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
|
<form method="post" action="/poista">
|
||||||
>Muokkaa</a>
|
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
||||||
<form method="post" action="/poista">
|
<button class="btn del" type="submit">Kyllä, poista</button>
|
||||||
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
</form>
|
||||||
<button class="btn del" type="submit">Poista</button>
|
<a class="btn" href={ templ.SafeURL(dayURL("/", v.Date, v.Today)) }>Peruuta</a>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
} else {
|
||||||
|
<div class="pair">
|
||||||
|
<a
|
||||||
|
class="btn"
|
||||||
|
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
|
||||||
|
>Muokkaa</a>
|
||||||
|
<a
|
||||||
|
class="btn del"
|
||||||
|
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "poista=1") }
|
||||||
|
>Poista</a>
|
||||||
|
</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.
|
||||||
<form method="post" action="/ruuat/poista">
|
templ rowActions(v catalogView, editURL string, id int64, kind string) {
|
||||||
<input type="hidden" name="id" value={ strconv.FormatInt(id, 10) }/>
|
if v.DeleteID == id && v.DeleteKind == kind {
|
||||||
<input type="hidden" name="tyyppi" value={ kind }/>
|
<div class="rowactions confirming">
|
||||||
<button type="submit" class="del" aria-label="Poista">Poista</button>
|
<span>Poista?</span>
|
||||||
</form>
|
<form method="post" action="/ruuat/poista">
|
||||||
</div>
|
<input type="hidden" name="id" value={ strconv.FormatInt(id, 10) }/>
|
||||||
|
<input type="hidden" name="tyyppi" value={ kind }/>
|
||||||
|
<button type="submit" class="del">Kyllä</button>
|
||||||
|
</form>
|
||||||
|
<a href="/ruuat">Peruuta</a>
|
||||||
|
</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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user