commit ee08ca35ae805a249460a1a1ee93c3d2a99de05e
parent 7f609f8ce63e8ab8c3c8e78609ca16e35c4e79bb
Author: breadcat <breadcat@users.noreply.github.com>
Date: Thu, 23 Jul 2026 15:55:20 +0100
I actually hate that month scrolling effect
Diffstat:
| M | main.go | | | 125 | +++++++++++++++++++++++++++++++++++++++++++++---------------------------------- |
| M | static/app.js | | | 61 | +++++++++++++++---------------------------------------------- |
| M | static/index.html | | | 13 | ++++++++++--- |
| M | static/style.css | | | 66 | +++++++++++++++++++++++++++++++++++++++--------------------------- |
4 files changed, 135 insertions(+), 130 deletions(-)
diff --git a/main.go b/main.go
@@ -232,56 +232,72 @@ func renderDaysRow(offset int) string {
return b.String()
}
-func renderWeekRow(weekOffset int) string {
+// monthBounds returns the first and last day of the target month, where
+// offset 0 = the current calendar month, -1 = previous month, 1 = next
+// month, etc.
+func monthBounds(offset int) (time.Time, time.Time) {
now := time.Now()
- wd := int(now.Weekday())
- if wd == 0 {
- wd = 7
+ firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
+ target := firstOfMonth.AddDate(0, offset, 0)
+ last := target.AddDate(0, 1, -1)
+ return target, last
+}
+
+func monthLabel(offset int) string {
+ target, _ := monthBounds(offset)
+ return target.Format("January 2006")
+}
+
+// renderCalendarTile renders a single day tile for the full-month grid. Days
+// that fall outside the target month (used to pad out the leading/trailing
+// weeks so the grid always shows whole weeks) get an "other-month" class so
+// they can be dimmed in CSS.
+func renderCalendarTile(date string, dayNum int, units float64, otherMonth bool) string {
+ cls := dateColorClass(units, date)
+ if otherMonth {
+ cls += " other-month"
}
- monday := now.AddDate(0, 0, -(wd-1)-(weekOffset*7))
- var b strings.Builder
- for i := 0; i < 7; i++ {
- d := monday.AddDate(0, 0, i)
- ds := d.Format("2006-01-02")
- b.WriteString(renderTile(ds, d.Format("Mon 2"), dayUnits(ds)))
+ todayCls := ""
+ if date == time.Now().Format("2006-01-02") {
+ todayCls = " today"
}
- return b.String()
+ unitsSpan := ""
+ if units > 0 {
+ unitsSpan = fmt.Sprintf(`<span class="units">%s u</span>`, formatUnits(units))
+ }
+ return fmt.Sprintf(
+ `<div class="tile %s%s" onclick="openDay('%s')" title="%s"><span class="date-label">%d</span>%s</div>`,
+ cls, todayCls, date, date, dayNum, unitsSpan,
+ )
}
-func weekLabel(offset int) string {
- now := time.Now()
- wd := int(now.Weekday())
+// renderMonthGrid renders a full calendar month (Monday-first weeks),
+// padded at the start/end with days from the adjacent months so every row
+// is a complete week. offset 0 = current month.
+func renderMonthGrid(offset int) string {
+ first, last := monthBounds(offset)
+
+ wd := int(first.Weekday())
if wd == 0 {
wd = 7
}
- monday := now.AddDate(0, 0, -(wd-1)-(offset*7))
- sunday := monday.AddDate(0, 0, 6)
- return monday.Format("2 Jan") + " – " + sunday.Format("2 Jan")
-}
-
-// renderWeekBlock renders a single labeled week row (7 tiles) for use in the
-// scrollable month view. weekOffset 0 = current week, 1 = last week, etc.
-func renderWeekBlock(weekOffset int) string {
- return `<div class="week-block" data-week-offset="` + strconv.Itoa(weekOffset) + `">` +
- `<div class="week-block-label">` + weekLabel(weekOffset) + `</div>` +
- `<div class="month-week-row">` + renderWeekRow(weekOffset) + `</div>` +
- `</div>`
-}
+ start := first.AddDate(0, 0, -(wd - 1))
-// renderMonthWeeks renders `count` consecutive week-blocks, for offsets
-// [start, start+count-1], ordered top-to-bottom from oldest to newest so
-// that the most recent (smallest offset) week always ends up last/at the
-// bottom of the scrollable container.
-func renderMonthWeeks(start, count int) string {
- if start < 0 {
- start = 0
- }
- if count <= 0 {
- count = 1
+ wd2 := int(last.Weekday())
+ if wd2 == 0 {
+ wd2 = 7
}
+ end := last.AddDate(0, 0, 7-wd2)
+
var b strings.Builder
- for off := start + count - 1; off >= start; off-- {
- b.WriteString(renderWeekBlock(off))
+ for d := start; !d.After(end); d = d.AddDate(0, 0, 7) {
+ b.WriteString(`<div class="month-week-row">`)
+ for i := 0; i < 7; i++ {
+ day := d.AddDate(0, 0, i)
+ ds := day.Format("2006-01-02")
+ b.WriteString(renderCalendarTile(ds, day.Day(), dayUnits(ds), day.Month() != first.Month()))
+ }
+ b.WriteString(`</div>`)
}
return b.String()
}
@@ -368,7 +384,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
page := string(tmpl)
page = strings.ReplaceAll(page, "{{SUMMARY}}", renderSummary())
page = strings.ReplaceAll(page, "{{DAYS_TILES}}", renderDaysRow(0))
- page = strings.ReplaceAll(page, "{{MONTH_WEEKS}}", renderMonthWeeks(0, monthBatchSize))
+ page = strings.ReplaceAll(page, "{{MONTH_LABEL}}", monthLabel(0))
+ page = strings.ReplaceAll(page, "{{MONTH_GRID}}", renderMonthGrid(0))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, page)
}
@@ -385,19 +402,19 @@ func handleTilesDays(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, renderDaysRow(offset))
}
-// monthBatchSize is how many week-rows are rendered per scroll batch in the
-// month view (both the initial server-rendered load and each subsequent
-// lazy-loaded chunk as the user scrolls up towards older weeks).
-const monthBatchSize = 6
-
-func handleTilesMonthWeeks(w http.ResponseWriter, r *http.Request) {
- start, _ := strconv.Atoi(r.URL.Query().Get("start"))
- count, err := strconv.Atoi(r.URL.Query().Get("count"))
- if err != nil || count <= 0 {
- count = monthBatchSize
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprint(w, renderMonthWeeks(start, count))
+func handleTilesMonth(w http.ResponseWriter, r *http.Request) {
+ offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
+ resp := struct {
+ Label string `json:"label"`
+ Grid string `json:"grid"`
+ NextDisabled bool `json:"next_disabled"`
+ }{
+ Label: monthLabel(offset),
+ Grid: renderMonthGrid(offset),
+ NextDisabled: offset >= 0,
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(resp)
}
func handleModal(w http.ResponseWriter, r *http.Request) {
@@ -520,7 +537,7 @@ func main() {
mux.HandleFunc("/", handleIndex)
mux.HandleFunc("/summary", handleSummary)
mux.HandleFunc("/tiles/days", handleTilesDays)
- mux.HandleFunc("/tiles/monthweeks", handleTilesMonthWeeks)
+ mux.HandleFunc("/tiles/month", handleTilesMonth)
mux.HandleFunc("/modal", handleModal)
mux.HandleFunc("/drink/add", handleAddDrink)
mux.HandleFunc("/drink/remove", handleRemoveDrink)
diff --git a/static/app.js b/static/app.js
@@ -2,11 +2,8 @@
let daysOffset = 0;
-// Month (scrollable weeks) view
-const MONTH_BATCH = 6; // must match monthBatchSize in main.go
-let monthWeeksLoaded = MONTH_BATCH; // weeks already present server-side on load
-let monthScrollReady = false; // becomes true once the panel is first opened
-let monthLoadingMore = false;
+// Month (calendar) view
+let monthOffset = 0; // 0 = current month, -1 = previous month, 1 = next month, ...
// Labels
@@ -28,7 +25,7 @@ async function fetchHTML(url, containerId) {
async function refreshAllTiles() {
await Promise.all([
fetchHTML('/tiles/days?offset=' + daysOffset, 'days-row'),
- refreshMonthScroll(),
+ loadMonth(),
]);
}
@@ -42,49 +39,21 @@ async function shiftDays(dir) {
await fetchHTML('/tiles/days?offset=' + daysOffset, 'days-row');
}
-// Month (scrollable weeks)
-
-// Called once, the first time the "Month view" <details> panel is opened.
-// Waits until the panel is actually visible (so scrollHeight is meaningful),
-// then jumps the scroll position to the bottom so the current week is the
-// last row in view, and wires up infinite-scroll-upward loading.
-function handleMonthDetailsToggle(details) {
- if (!details.open || monthScrollReady) return;
- monthScrollReady = true;
- const container = document.getElementById('month-scroll');
- if (!container) return;
- container.scrollTop = container.scrollHeight;
- container.addEventListener('scroll', onMonthScroll);
-}
+// Month (calendar)
-async function onMonthScroll(e) {
- const container = e.target;
- if (container.scrollTop > 60 || monthLoadingMore) return;
- monthLoadingMore = true;
- const prevHeight = container.scrollHeight;
- const html = await (await fetch(
- '/tiles/monthweeks?start=' + monthWeeksLoaded + '&count=' + MONTH_BATCH
- )).text();
- if (html.trim()) {
- container.insertAdjacentHTML('afterbegin', html);
- monthWeeksLoaded += MONTH_BATCH;
- // Keep the same rows in view instead of jumping after prepending.
- container.scrollTop = container.scrollHeight - prevHeight + container.scrollTop;
- }
- monthLoadingMore = false;
+// Fetches the full calendar grid + label for the currently selected
+// monthOffset and swaps it in, updating the Prev/Next button state.
+async function loadMonth() {
+ const res = await fetch('/tiles/month?offset=' + monthOffset);
+ const data = await res.json();
+ document.getElementById('month-label').textContent = data.label;
+ document.getElementById('month-grid').innerHTML = data.grid;
+ document.getElementById('month-next-btn').disabled = data.next_disabled;
}
-// Re-fetches the same number of weeks already loaded (e.g. after a drink is
-// added/removed) and swaps them in, preserving the current scroll position.
-async function refreshMonthScroll() {
- const container = document.getElementById('month-scroll');
- if (!container) return;
- const prevScrollTop = container.scrollTop;
- const html = await (await fetch(
- '/tiles/monthweeks?start=0&count=' + monthWeeksLoaded
- )).text();
- container.innerHTML = html;
- container.scrollTop = prevScrollTop;
+async function shiftMonth(dir) {
+ monthOffset += dir;
+ await loadMonth();
}
// Days
diff --git a/static/index.html b/static/index.html
@@ -33,13 +33,20 @@
<div class="tiles-row" id="days-row">{{DAYS_TILES}}</div>
</div>
-<details ontoggle="handleMonthDetailsToggle(this)" open>
+<details open>
<summary>
Month view
- <span class="panel-sublabel">Scroll up for earlier weeks</span>
</summary>
<div class="details-inner">
- <div class="month-scroll" id="month-scroll">{{MONTH_WEEKS}}</div>
+ <div class="month-nav">
+ <button class="btn-nav" onclick="shiftMonth(-1)">◀ Prev</button>
+ <span class="month-label" id="month-label">{{MONTH_LABEL}}</span>
+ <button class="btn-nav" id="month-next-btn" onclick="shiftMonth(1)" disabled>Next ▶</button>
+ </div>
+ <div class="month-weekday-header">
+ <span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span><span>Sun</span>
+ </div>
+ <div class="month-grid" id="month-grid">{{MONTH_GRID}}</div>
</div>
</details>
diff --git a/static/style.css b/static/style.css
@@ -134,41 +134,47 @@ details > summary::after { content: "▸"; transition: transform 0.2s; }
details[open] > summary::after { transform: rotate(90deg); }
details > .details-inner { padding: 0.75rem 1rem 1rem; overflow-x: auto; }
-.panel-sublabel {
- font-size: 0.75rem;
- color: #666;
- font-weight: 400;
- margin-left: auto;
- margin-right: 1rem;
-}
-
-/* Month view (scrollable weeks) */
+/* Month view (calendar grid) */
-.month-scroll {
+.month-nav {
display: flex;
- flex-direction: column;
- gap: 12px;
- max-height: 380px;
- overflow-y: auto;
- padding-right: 4px;
- scroll-behavior: auto;
- touch-action: pan-y;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 0.75rem;
}
-.month-scroll::-webkit-scrollbar { width: 5px; }
-.month-scroll::-webkit-scrollbar-track { background: transparent; }
-.month-scroll::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
+.month-label {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text);
+ text-align: center;
+ flex: 1;
+}
-.week-block-label {
- font-size: 0.7rem;
+.month-weekday-header {
+ display: grid;
+ grid-template-columns: repeat(7, minmax(0, 1fr));
+ gap: 6px;
+ margin-bottom: 6px;
+}
+
+.month-weekday-header span {
+ font-size: 0.65rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
color: var(--text-dim);
- margin-bottom: 5px;
+ text-align: center;
}
-/* Week rows inside the month scroller use a fluid 7-column grid (instead of
- the fixed viewport-based tile size used elsewhere) so they always fit
- exactly within the panel's width — including the width taken up by the
- vertical scrollbar — with no per-row horizontal scrollbar. */
+.month-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+/* Week rows in the calendar grid use a fluid 7-column grid so they always
+ fit exactly within the panel's width with no horizontal scrollbar. */
.month-week-row {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
@@ -181,6 +187,12 @@ details > .details-inner { padding: 0.75rem 1rem 1rem; overflow-x: auto; }
aspect-ratio: 1 / 1;
}
+/* Days padded in from the previous/next month, shown dimmed to keep the
+ grid full without drawing focus away from the current month. */
+.tile.other-month {
+ opacity: 0.35;
+}
+
/* Modal */
.modal-overlay {