Release: leftovers, the real dish list, and a day list that stays put #2
+30
-14
@@ -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)
|
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
|
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)
|
log.Printf("history: %v", err)
|
||||||
}
|
}
|
||||||
|
// Nothing logged ever: the selected day is still the one being worked on,
|
||||||
render(w, r, logPage(v))
|
// 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
|
// 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 {
|
if v.Special, err = listSpecial(a.db, v.Search); err != nil {
|
||||||
log.Printf("list special: %v", err)
|
log.Printf("list special: %v", err)
|
||||||
}
|
}
|
||||||
v.HistoryDays = historyWindow(r)
|
a.loadDays(r, &v)
|
||||||
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)
|
|
||||||
}
|
|
||||||
render(w, r, logPage(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, "?") {
|
if strings.Contains(target, "?") {
|
||||||
sep = "&"
|
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.
|
// 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)
|
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) {
|
func (a *app) redirectToDay(w http.ResponseWriter, r *http.Request, date time.Time) {
|
||||||
target := "/"
|
http.Redirect(w, r, dayLink(date, today(a.loc)), http.StatusSeeOther)
|
||||||
if !date.Equal(today(a.loc)) {
|
|
||||||
target += "?pvm=" + date.Format(dateLayout)
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, target, http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mainForm and sideForm carry what the user typed, so a rejected submission
|
// mainForm and sideForm carry what the user typed, so a rejected submission
|
||||||
|
|||||||
@@ -326,8 +326,26 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; }
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
/* History sits under the logger on the same page, so whole rows are links. */
|
/* The day list is the page; rows are links and the selected one expands. */
|
||||||
.history { margin-top: 8px; }
|
.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 {
|
.entry, .gapline {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
+57
-28
@@ -43,6 +43,18 @@ func dayURL(base string, d, now time.Time) string {
|
|||||||
return base + "?pvm=" + isoDate(d)
|
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
|
// pickSeparator joins a dish onto a day URL, which already carries ?pvm= for
|
||||||
// any day but today.
|
// any day but today.
|
||||||
func pickSeparator(v logView) string {
|
func pickSeparator(v logView) string {
|
||||||
@@ -52,6 +64,17 @@ func pickSeparator(v logView) string {
|
|||||||
return "&"
|
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
|
// jsString renders a Go string as a JavaScript literal, for the data-signals
|
||||||
// attribute that seeds the search box.
|
// attribute that seeds the search box.
|
||||||
func jsString(s string) string {
|
func jsString(s string) string {
|
||||||
@@ -281,6 +304,30 @@ templ logPage(v logView) {
|
|||||||
@daySwitch(v)
|
@daySwitch(v)
|
||||||
</header>
|
</header>
|
||||||
<main class="pad">
|
<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 {
|
switch {
|
||||||
case v.Chosen != nil:
|
case v.Chosen != nil:
|
||||||
@sidesStep(v)
|
@sidesStep(v)
|
||||||
@@ -289,26 +336,9 @@ templ logPage(v logView) {
|
|||||||
default:
|
default:
|
||||||
@loggedCard(v)
|
@loggedCard(v)
|
||||||
}
|
}
|
||||||
@historyList(v)
|
</div>
|
||||||
</main>
|
} else if row.Entry != nil {
|
||||||
}
|
<a class="entry" href={ templ.SafeURL(dayLink(row.Date, v.Today)) }>
|
||||||
}
|
|
||||||
|
|
||||||
// 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)) }>
|
|
||||||
<time>{ dayLabelFI(row.Date) }</time>
|
<time>{ dayLabelFI(row.Date) }</time>
|
||||||
<div>
|
<div>
|
||||||
<div class="nm">
|
<div class="nm">
|
||||||
@@ -320,14 +350,13 @@ templ historyList(v logView) {
|
|||||||
<span class="chev">›</span>
|
<span class="chev">›</span>
|
||||||
</a>
|
</a>
|
||||||
} else {
|
} 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>
|
<time>{ dayLabelFI(row.Date) }</time>
|
||||||
<span>Ei merkintää</span>
|
<span>Ei merkintää</span>
|
||||||
<span class="act">Merkitse</span>
|
<span class="act">Merkitse</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if v.History.More {
|
if v.History.More {
|
||||||
<a
|
<a
|
||||||
class="more"
|
class="more"
|
||||||
@@ -406,7 +435,7 @@ templ boardList(v logView) {
|
|||||||
for _, d := range v.Special {
|
for _, d := range v.Special {
|
||||||
<a
|
<a
|
||||||
class="pill plain"
|
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())
|
@categoryIcon(d.CategoryKey())
|
||||||
{ d.Name }
|
{ d.Name }
|
||||||
@@ -473,7 +502,7 @@ templ quickAddCard(v logView) {
|
|||||||
templ dishPill(d Dish, v logView) {
|
templ dishPill(d Dish, v logView) {
|
||||||
<a
|
<a
|
||||||
class={ "pill", d.Size() }
|
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())
|
@categoryIcon(d.CategoryKey())
|
||||||
{ d.Name }
|
{ d.Name }
|
||||||
@@ -511,7 +540,7 @@ templ sidesStep(v logView) {
|
|||||||
}
|
}
|
||||||
<button class="primary" type="submit">Tallenna</button>
|
<button class="primary" type="submit">Tallenna</button>
|
||||||
</form>
|
</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>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,17 +565,17 @@ templ loggedCard(v logView) {
|
|||||||
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
||||||
<button class="btn del" type="submit">Kyllä, poista</button>
|
<button class="btn del" type="submit">Kyllä, poista</button>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<div class="pair">
|
<div class="pair">
|
||||||
<a
|
<a
|
||||||
class="btn"
|
class="btn"
|
||||||
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
|
href={ templ.SafeURL(stepURL(v, "muuta=1")) }
|
||||||
>Muokkaa</a>
|
>Muokkaa</a>
|
||||||
<a
|
<a
|
||||||
class="btn del"
|
class="btn del"
|
||||||
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "poista=1") }
|
href={ templ.SafeURL(stepURL(v, "poista=1")) }
|
||||||
>Poista</a>
|
>Poista</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -137,8 +137,16 @@ check "saving redirects back to the day" \
|
|||||||
check "the saved day shows what was eaten" \
|
check "the saved day shows what was eaten" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")" "kirjattu"
|
"$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")" "kirjattu"
|
||||||
|
|
||||||
check "history is on the same page as the logger" \
|
# The selected day expands inside the list rather than in a panel above it,
|
||||||
"$(curl -s -u ":$pass" "http://$addr/")" "Aiemmin"
|
# 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.
|
# 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")
|
saved=$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05&poista=1")
|
||||||
|
|||||||
Reference in New Issue
Block a user