diff --git a/cmd/foodster/handlers.go b/cmd/foodster/handlers.go index 3428392..85062f7 100644 --- a/cmd/foodster/handlers.go +++ b/cmd/foodster/handlers.go @@ -98,16 +98,56 @@ type logView struct { Confirming bool } -func (a *app) index(w http.ResponseWriter, r *http.Request) { - date := a.date(r) - v := logView{ - Date: date, - Today: today(a.loc), - Search: strings.TrimSpace(r.URL.Query().Get("haku")), - Checked: map[int64]bool{}, - } +// logOptions is what the Kirjaa screen is being asked to show. Pulled out of +// the request for a page load or a patch, and set directly after a write, +// where the answer is simply "that day, nothing else open". +type logOptions struct { + Date time.Time + Dish string // ?ruoka=, opening the sides step + Changing bool // ?muuta=, swapping the dish on a logged day + Confirming bool // ?poista=, asking before deleting the entry + Search string +} - v.Confirming = r.URL.Query().Get("poista") != "" +func (a *app) logOptionsFrom(r *http.Request) logOptions { + q := r.URL.Query() + return logOptions{ + Date: a.date(r), + Dish: q.Get("ruoka"), + Changing: q.Get("muuta") != "", + Confirming: q.Get("poista") != "", + Search: strings.TrimSpace(q.Get("haku")), + } +} + +func (a *app) index(w http.ResponseWriter, r *http.Request) { + render(w, r, logPage(a.buildLog(r, a.logOptionsFrom(r)))) +} + +// day patches the list in place. Every link in it calls here rather than +// loading a page, so opening a day leaves the scroll position alone. +func (a *app) day(w http.ResponseWriter, r *http.Request) { + fragment(w, r, dayList(a.buildLog(r, a.logOptionsFrom(r)))) +} + +// finishDay answers a write: a patch for Datastar, a redirect otherwise. +func (a *app) finishDay(w http.ResponseWriter, r *http.Request, date time.Time) { + if isDatastar(r) { + fragment(w, r, dayList(a.buildLog(r, logOptions{Date: date}))) + return + } + a.redirectToDay(w, r, date) +} + +func (a *app) buildLog(r *http.Request, o logOptions) logView { + date := o.Date + v := logView{ + Date: date, + Today: today(a.loc), + Search: o.Search, + Checked: map[int64]bool{}, + Confirming: o.Confirming, + } entry, err := entryFor(a.db, date) if err != nil { @@ -118,8 +158,8 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) { // ?ruoka= opens the sides step for that dish. When it is the dish already // logged, the existing sides come back ticked, which makes editing an // entry the same screen as creating one. - if raw := r.URL.Query().Get("ruoka"); raw != "" { - if id, err := strconv.ParseInt(raw, 10, 64); err == nil { + if o.Dish != "" { + if id, err := strconv.ParseInt(o.Dish, 10, 64); err == nil { if dish, err := dishByID(a.db, id); err == nil { v.Chosen = dish if entry != nil && entry.Main.ID == id { @@ -134,7 +174,7 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) { // The board shows when there is nothing logged yet, or when the entry is // being changed. "Muokkaa" on a logged day sets ?muuta=1 and lands here, // so swapping the dish and picking one for the first time are one path. - changing := r.URL.Query().Get("muuta") != "" || v.Search != "" + changing := o.Changing || v.Search != "" if v.Chosen == nil && (v.Entry == nil || changing) { v.ShowBoard = true if v.Dishes, err = listDishes(a.db, v.Search); err != nil { @@ -157,7 +197,7 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) { } a.loadDays(r, &v) - render(w, r, logPage(v)) + return v } // loadDays fills the day list. The selected day expands inside it rather than @@ -344,6 +384,12 @@ func (a *app) quickAdd(w http.ResponseWriter, r *http.Request) { log.Printf("quick add: %v", err) form.Err = "Tallennus epäonnistui." default: + // Created: straight on to its sides step. + opts := logOptions{Date: date, Dish: strconv.FormatInt(id, 10)} + if isDatastar(r) { + fragment(w, r, dayList(a.buildLog(r, opts))) + return + } a.redirectToPick(w, r, date, id) return } @@ -351,23 +397,12 @@ func (a *app) quickAdd(w http.ResponseWriter, r *http.Request) { // Rejected: back to the board with the form filled in and the search // still narrowed, so the add card stays on screen. - v := logView{ - Date: date, - Today: today(a.loc), - Search: form.Name, - Checked: map[int64]bool{}, - New: form, - ShowBoard: true, + v := a.buildLog(r, logOptions{Date: date, Search: form.Name}) + v.New = form + if isDatastar(r) { + fragment(w, r, dayList(v)) + return } - var err error - if v.Dishes, err = listDishes(a.db, v.Search); err != nil { - log.Printf("list dishes: %v", err) - } - v.Groups = groupDishes(v.Dishes) - if v.Special, err = listSpecial(a.db, v.Search); err != nil { - log.Printf("list special: %v", err) - } - a.loadDays(r, &v) render(w, r, logPage(v)) } @@ -377,9 +412,7 @@ 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)+"#"+dayAnchor(date), - http.StatusSeeOther) + http.Redirect(w, r, target+sep+"ruoka="+strconv.FormatInt(id, 10), http.StatusSeeOther) } // save records the meal and redirects, so a refresh cannot double-post. @@ -404,7 +437,7 @@ func (a *app) save(w http.ResponseWriter, r *http.Request) { http.Error(w, "tallennus epäonnistui", http.StatusInternalServerError) return } - a.redirectToDay(w, r, date) + a.finishDay(w, r, date) } func (a *app) delete(w http.ResponseWriter, r *http.Request) { @@ -414,13 +447,12 @@ func (a *app) delete(w http.ResponseWriter, r *http.Request) { http.Error(w, "poisto epäonnistui", http.StatusInternalServerError) return } - a.redirectToDay(w, r, date) + a.finishDay(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. +// redirectToDay is the no-JavaScript path back after a write. func (a *app) redirectToDay(w http.ResponseWriter, r *http.Request, date time.Time) { - http.Redirect(w, r, dayLink(date, today(a.loc)), http.StatusSeeOther) + http.Redirect(w, r, dayURL("/", date, today(a.loc)), http.StatusSeeOther) } // mainForm and sideForm carry what the user typed, so a rejected submission diff --git a/cmd/foodster/main.go b/cmd/foodster/main.go index d6d9051..0ed12ee 100644 --- a/cmd/foodster/main.go +++ b/cmd/foodster/main.go @@ -159,6 +159,7 @@ func routes(db *sql.DB, loc *time.Location, password string) http.Handler { mux.HandleFunc("POST /kirjaa", a.save) mux.HandleFunc("POST /lisaa", a.quickAdd) mux.HandleFunc("GET /etsi", a.searchBoard) + mux.HandleFunc("GET /paiva", a.day) mux.HandleFunc("POST /poista", a.delete) mux.HandleFunc("GET /ruuat", a.catalog) mux.HandleFunc("GET /ruuat/etsi", a.searchCatalog) diff --git a/cmd/foodster/views.templ b/cmd/foodster/views.templ index da2f85d..d93c09f 100644 --- a/cmd/foodster/views.templ +++ b/cmd/foodster/views.templ @@ -49,16 +49,15 @@ func showURL(pageURL string) string { return strings.Replace(pageURL, "/ruuat?", "/ruuat/nayta?", 1) } -// 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) +// dayPatch is the endpoint behind every link in the day list. The href beside +// it stays a real page URL for anyone without JavaScript; Datastar calls this +// instead and swaps the list where it stands. +func dayPatch(d time.Time, param string) string { + url := "/paiva?pvm=" + isoDate(d) + if param != "" { + url += "&" + param + } + return url } // pickSeparator joins a dish onto a day URL, which already carries ?pvm= for @@ -70,15 +69,14 @@ 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. +// stepURL is the page URL for a link inside the open day: the fallback when +// there is no JavaScript to intercept it. func stepURL(v logView, param string) string { url := dayURL("/", v.Date, v.Today) if param != "" { url += pickSeparator(v) + param } - return url + "#" + dayAnchor(v.Date) + return url } // jsString renders a Go string as a JavaScript literal, for the data-signals @@ -320,13 +318,13 @@ templ logPage(v logView) { // 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) { -
+
for i, row := range v.History.Rows { if i == 0 || v.History.Rows[i-1].Date.Month() != row.Date.Month() {

{ monthFI(row.Date) }

} if row.Date.Equal(v.Date) { -
@@ -442,6 +449,7 @@ templ boardList(v logView) { @categoryIcon(d.CategoryKey()) { d.Name } @@ -477,7 +485,11 @@ templ quickAddCard(v logView) { if v.New.Err != "" {

{ v.New.Err }

} -
+ @categoryIcon(d.CategoryKey()) { d.Name } @@ -524,7 +537,11 @@ templ sidesStep(v logView) { @categoryIcon(v.Chosen.CategoryKey()) { v.Chosen.Name } - + if v.Chosen.HasSides && len(v.Sides) > 0 { @@ -546,7 +563,11 @@ templ sidesStep(v logView) { } -
Peruuta + Peruuta
} @@ -567,21 +588,31 @@ templ loggedCard(v logView) { if v.Confirming {

Poistetaanko merkintä?

-
+
- Peruuta + Peruuta
} else {
Muokkaa Poista
} diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 2005054..1145601 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -140,13 +140,31 @@ check "the saved day shows what was eaten" \ # 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 "the selected day expands in place" "$day" 'class="open"' 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" \ +# ---- the day list patches in place instead of navigating ---------------- + +dayp=$(curl -s -u ":$pass" -H 'Datastar-Request: true' "http://$addr/paiva?pvm=2026-09-05") +check "opening a day patches the list" "$dayp" 'id="paivat"' +refute "and returns a fragment, not a page" "$dayp" "