limoncello

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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:
Mmain.go | 125+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mstatic/app.js | 61+++++++++++++++----------------------------------------------
Mstatic/index.html | 13++++++++++---
Mstatic/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 {