diff --git a/cmd/foodster/handlers.go b/cmd/foodster/handlers.go index 2f72e7e..d14ace6 100644 --- a/cmd/foodster/handlers.go +++ b/cmd/foodster/handlers.go @@ -156,13 +156,33 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) { } } + a.loadDays(r, &v) + render(w, r, logPage(v)) +} + +// loadDays fills the day list. The selected day expands inside it rather than +// in a panel above it, so choosing a day from the list does not reorder the +// list underneath the tap. +func (a *app) loadDays(r *http.Request, v *logView) { v.HistoryDays = historyWindow(r) + + // The window has to reach the selected day, or it would have nowhere to + // expand. + if reach := int(v.Today.Sub(v.Date).Hours()/24) + 1; reach > v.HistoryDays { + v.HistoryDays = min(reach, maxHistoryDays) + } v.HistoryMore = v.HistoryDays + historyDays - if v.History, err = history(a.db, a.loc, today(a.loc), v.HistoryDays); err != nil { + + page, err := history(a.db, a.loc, v.Today, v.HistoryDays) + if err != nil { log.Printf("history: %v", err) } - - render(w, r, logPage(v)) + // Nothing logged ever: the selected day is still the one being worked on, + // so it needs a row of its own to open in. + if len(page.Rows) == 0 { + page.Rows = []HistoryRow{{Date: v.Date, Entry: v.Entry}} + } + v.History = page } // searchSignals is what Datastar sends back: for a GET it JSON-encodes the @@ -303,11 +323,7 @@ func (a *app) quickAdd(w http.ResponseWriter, r *http.Request) { if v.Special, err = listSpecial(a.db, v.Search); err != nil { log.Printf("list special: %v", err) } - v.HistoryDays = historyWindow(r) - v.HistoryMore = v.HistoryDays + historyDays - if v.History, err = history(a.db, a.loc, today(a.loc), v.HistoryDays); err != nil { - log.Printf("history: %v", err) - } + a.loadDays(r, &v) render(w, r, logPage(v)) } @@ -317,7 +333,9 @@ func (a *app) redirectToPick(w http.ResponseWriter, r *http.Request, date time.T if strings.Contains(target, "?") { sep = "&" } - http.Redirect(w, r, target+sep+"ruoka="+strconv.FormatInt(id, 10), http.StatusSeeOther) + http.Redirect(w, r, + target+sep+"ruoka="+strconv.FormatInt(id, 10)+"#"+dayAnchor(date), + http.StatusSeeOther) } // save records the meal and redirects, so a refresh cannot double-post. @@ -355,12 +373,10 @@ func (a *app) delete(w http.ResponseWriter, r *http.Request) { a.redirectToDay(w, r, date) } +// redirectToDay returns to the day in the list, anchor included, so saving or +// deleting leaves the viewport where the work was happening. func (a *app) redirectToDay(w http.ResponseWriter, r *http.Request, date time.Time) { - target := "/" - if !date.Equal(today(a.loc)) { - target += "?pvm=" + date.Format(dateLayout) - } - http.Redirect(w, r, target, http.StatusSeeOther) + http.Redirect(w, r, dayLink(date, today(a.loc)), http.StatusSeeOther) } // mainForm and sideForm carry what the user typed, so a rejected submission diff --git a/cmd/foodster/static/app.css b/cmd/foodster/static/app.css index 30de39d..415d8ca 100644 --- a/cmd/foodster/static/app.css +++ b/cmd/foodster/static/app.css @@ -326,8 +326,26 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; } text-transform: uppercase; color: var(--muted); } -/* History sits under the logger on the same page, so whole rows are links. */ -.history { margin-top: 8px; } +/* The day list is the page; rows are links and the selected one expands. */ +.history { margin-top: 4px; } + +/* Marked with a bar down the side, not rules above and below: the rows either + side already draw a bottom border, so a horizontal rule here doubled up. + scroll-margin keeps the anchor off the viewport edge. */ +.open { + scroll-margin-top: 12px; + margin: 8px 0 18px; + padding: 10px 0 4px 13px; + border-left: 3px solid var(--accent); +} +.openday { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--accent); +} .entry, .gapline { display: flex; gap: 12px; diff --git a/cmd/foodster/views.templ b/cmd/foodster/views.templ index 3cd4d00..fa33b29 100644 --- a/cmd/foodster/views.templ +++ b/cmd/foodster/views.templ @@ -43,6 +43,18 @@ func dayURL(base string, d, now time.Time) string { return base + "?pvm=" + isoDate(d) } +// dayAnchor names the element a day expands into. +func dayAnchor(d time.Time) string { + return "paiva-" + isoDate(d) +} + +// dayLink opens a day and lands the viewport on it. Without the fragment the +// browser would jump to the top of a page whose selected day might be far +// down the list. +func dayLink(d, now time.Time) string { + return dayURL("/", d, now) + "#" + dayAnchor(d) +} + // pickSeparator joins a dish onto a day URL, which already carries ?pvm= for // any day but today. func pickSeparator(v logView) string { @@ -52,6 +64,17 @@ func pickSeparator(v logView) string { return "&" } +// stepURL is any link inside the open day. It keeps the anchor, so picking a +// dish or cancelling stays where the day is instead of throwing the viewport +// back to the top of the list. +func stepURL(v logView, param string) string { + url := dayURL("/", v.Date, v.Today) + if param != "" { + url += pickSeparator(v) + param + } + return url + "#" + dayAnchor(v.Date) +} + // jsString renders a Go string as a JavaScript literal, for the data-signals // attribute that seeds the search box. func jsString(s string) string { @@ -281,51 +304,57 @@ templ logPage(v logView) { @daySwitch(v)
- switch { - case v.Chosen != nil: - @sidesStep(v) - case v.ShowBoard: - @board(v) - default: - @loggedCard(v) - } - @historyList(v) + @dayList(v)
} } -// historyList sits under the day being logged: the two were always one thing, -// since every row here is a link back into the logger above it. -templ historyList(v logView) { +// dayList is the whole page: every day back through the window, with the +// selected one expanded where it sits. Opening a day used to swap in a panel +// above the list and drop that day out of it, so the rows below jumped up +// under the tap. Now nothing moves — the row grows. +templ dayList(v logView) {
-

Aiemmin

- if len(v.History.Rows) == 0 { -

Ei vielä merkintöjä.

- } for i, row := range v.History.Rows { - if !row.Date.Equal(v.Date) { - if i == 0 || v.History.Rows[i-1].Date.Month() != row.Date.Month() { -

{ monthFI(row.Date) }

- } - if row.Entry != nil { - - -
-
- @categoryIcon(row.Entry.Main.CategoryKey()) - { row.Entry.Main.Name } -
-
{ row.Entry.SidesLabel() }
+ if i == 0 || v.History.Rows[i-1].Date.Month() != row.Date.Month() { +

{ monthFI(row.Date) }

+ } + if row.Date.Equal(v.Date) { +
+

+ if row.Date.Equal(v.Today) { + Tänään + } else { + { longDateFI(row.Date) } + } +

+ switch { + case v.Chosen != nil: + @sidesStep(v) + case v.ShowBoard: + @board(v) + default: + @loggedCard(v) + } +
+ } else if row.Entry != nil { +
+ +
+
+ @categoryIcon(row.Entry.Main.CategoryKey()) + { row.Entry.Main.Name }
- -
- } else { - - - Ei merkintää - Merkitse - - } +
{ row.Entry.SidesLabel() }
+
+ + + } else { + + + Ei merkintää + Merkitse + } } if v.History.More { @@ -406,7 +435,7 @@ templ boardList(v logView) { for _, d := range v.Special { Tallenna - Peruuta + Peruuta
} @@ -536,17 +565,17 @@ templ loggedCard(v logView) { - Peruuta + Peruuta } else {
Muokkaa Poista
} diff --git a/scripts/smoke.sh b/scripts/smoke.sh index e991721..05c9d46 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -137,8 +137,16 @@ check "saving redirects back to the day" \ check "the saved day shows what was eaten" \ "$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")" "kirjattu" -check "history is on the same page as the logger" \ - "$(curl -s -u ":$pass" "http://$addr/")" "Aiemmin" +# The selected day expands inside the list rather than in a panel above it, +# so the rows below do not shift when one is tapped. +day=$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05") +check "the selected day expands in place" "$day" 'id="paiva-2026-09-05"' +check "and stays in the list rather than being lifted out" "$day" "kirjattu" +check "links inside it keep the anchor" "$day" "#paiva-2026-09-05" + +check "saving returns to the day, not the top" \ + "$(curl -s -o /dev/null -w '%{redirect_url}' -u ":$pass" \ + -d "pvm=2026-09-04&ruoka=$ruoka" "http://$addr/kirjaa")" "#paiva-2026-09-04" # 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")