Expand the selected day where it sits

Choosing a day from the list made the page jump around. Two causes, both
structural.

The editing panel lived above the list, so opening a day swapped content in
at the top and pushed everything down. And the selected day was then skipped
when rendering the list — to avoid showing it twice — so every row below it
slid up under the tap.

The day list is now the page. The selected day expands in place and every
other row stays exactly where it was. Anchors carry the viewport to the day
rather than the top of the document, and links inside the open day keep the
anchor, so picking a dish or cancelling does not throw the page back up.
Saving and deleting redirect to the same anchor, leaving the viewport where
the work was happening. The history window stretches to reach the selected
day, which would otherwise have no row to open in.

The open day is marked with a bar down its left side rather than rules above
and below: the rows either side already draw a bottom border, so a horizontal
rule doubled up with it on any day but the first.
This commit is contained in:
Esa Kataja
2026-09-05 23:21:26 +03:00
parent d743327cd2
commit 768ab50140
4 changed files with 133 additions and 62 deletions
+30 -14
View File
@@ -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
+20 -2
View File
@@ -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;
+57 -28
View File
@@ -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,6 +304,30 @@ templ logPage(v logView) {
@daySwitch(v)
</header>
<main class="pad">
@dayList(v)
</main>
}
}
// 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) {
<section class="history">
for i, row := range v.History.Rows {
if i == 0 || v.History.Rows[i-1].Date.Month() != row.Date.Month() {
<p class="monthrule">{ monthFI(row.Date) }</p>
}
if row.Date.Equal(v.Date) {
<div class="open" id={ dayAnchor(row.Date) }>
<p class="openday">
if row.Date.Equal(v.Today) {
Tänään
} else {
{ longDateFI(row.Date) }
}
</p>
switch {
case v.Chosen != nil:
@sidesStep(v)
@@ -289,26 +336,9 @@ templ logPage(v logView) {
default:
@loggedCard(v)
}
@historyList(v)
</main>
}
}
// 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) {
<section class="history">
<h3 class="sechead">Aiemmin</h3>
if len(v.History.Rows) == 0 {
<p class="muted small">Ei vielä merkintöjä.</p>
}
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() {
<p class="monthrule">{ monthFI(row.Date) }</p>
}
if row.Entry != nil {
<a class="entry" href={ templ.SafeURL(dayURL("/", row.Date, v.Today)) }>
</div>
} else if row.Entry != nil {
<a class="entry" href={ templ.SafeURL(dayLink(row.Date, v.Today)) }>
<time>{ dayLabelFI(row.Date) }</time>
<div>
<div class="nm">
@@ -320,14 +350,13 @@ templ historyList(v logView) {
<span class="chev"></span>
</a>
} else {
<a class="gapline" href={ templ.SafeURL(dayURL("/", row.Date, v.Today)) }>
<a class="gapline" href={ templ.SafeURL(dayLink(row.Date, v.Today)) }>
<time>{ dayLabelFI(row.Date) }</time>
<span>Ei merkintää</span>
<span class="act">Merkitse</span>
</a>
}
}
}
if v.History.More {
<a
class="more"
@@ -406,7 +435,7 @@ templ boardList(v logView) {
for _, d := range v.Special {
<a
class="pill plain"
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "ruoka=" + strconv.FormatInt(d.ID, 10)) }
href={ templ.SafeURL(stepURL(v, "ruoka="+strconv.FormatInt(d.ID, 10))) }
>
@categoryIcon(d.CategoryKey())
{ d.Name }
@@ -473,7 +502,7 @@ templ quickAddCard(v logView) {
templ dishPill(d Dish, v logView) {
<a
class={ "pill", d.Size() }
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "ruoka=" + strconv.FormatInt(d.ID, 10)) }
href={ templ.SafeURL(stepURL(v, "ruoka="+strconv.FormatInt(d.ID, 10))) }
>
@categoryIcon(d.CategoryKey())
{ d.Name }
@@ -511,7 +540,7 @@ templ sidesStep(v logView) {
}
<button class="primary" type="submit">Tallenna</button>
</form>
<a class="ghost" href={ templ.SafeURL(dayURL("/", v.Date, v.Today)) }>Peruuta</a>
<a class="ghost" href={ templ.SafeURL(stepURL(v, "")) }>Peruuta</a>
</section>
}
@@ -536,17 +565,17 @@ templ loggedCard(v logView) {
<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>
<a class="btn" href={ templ.SafeURL(stepURL(v, "")) }>Peruuta</a>
</div>
} else {
<div class="pair">
<a
class="btn"
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
href={ templ.SafeURL(stepURL(v, "muuta=1")) }
>Muokkaa</a>
<a
class="btn del"
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "poista=1") }
href={ templ.SafeURL(stepURL(v, "poista=1")) }
>Poista</a>
</div>
}
+10 -2
View File
@@ -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")