From 1c6552117038cb7c01e016dbf1ac062e1d9f9c73 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 1 Feb 2026 10:52:28 -1000 Subject: Improve timeline view with dynamic bounds, now line, and overlap handling - Add dynamic calendar clipping: show 1 hour before/after events instead of hardcoded 6am-10pm - Add "NOW" line indicator showing current time position - Improve time label readability with larger font and better contrast - Add overlap detection with column-based indentation for concurrent events - Apply calendar view to Tomorrow section (matching Today's layout) - Fix auto-refresh switching to tasks tab (default was 'tasks' instead of 'timeline') Co-Authored-By: Claude Opus 4.5 --- internal/handlers/handlers.go | 7 ++- internal/handlers/handlers_test.go | 9 +++- internal/handlers/timeline.go | 87 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) (limited to 'internal') diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index bba12ad..c384c48 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -36,8 +36,13 @@ type Handler struct { // New creates a new Handler instance func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, cfg *config.Config) *Handler { + // Template functions + funcMap := template.FuncMap{ + "subtract": func(a, b int) int { return a - b }, + } + // Parse templates including partials - tmpl, err := template.ParseGlob(filepath.Join(cfg.TemplateDir, "*.html")) + tmpl, err := template.New("").Funcs(funcMap).ParseGlob(filepath.Join(cfg.TemplateDir, "*.html")) if err != nil { log.Printf("Warning: failed to parse templates: %v", err) } diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index 3367ef6..96cb911 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -64,11 +64,16 @@ func setupTestDB(t *testing.T) (*store.Store, func()) { func loadTestTemplates(t *testing.T) *template.Template { t.Helper() + // Template functions (must match handlers.go) + funcMap := template.FuncMap{ + "subtract": func(a, b int) int { return a - b }, + } + // Get path relative to project root - tmpl, err := template.ParseGlob(filepath.Join("web", "templates", "*.html")) + tmpl, err := template.New("").Funcs(funcMap).ParseGlob(filepath.Join("web", "templates", "*.html")) if err != nil { // Try from internal/handlers (2 levels up) - tmpl, err = template.ParseGlob(filepath.Join("..", "..", "web", "templates", "*.html")) + tmpl, err = template.New("").Funcs(funcMap).ParseGlob(filepath.Join("..", "..", "web", "templates", "*.html")) if err != nil { t.Logf("Warning: failed to parse templates: %v", err) return nil diff --git a/internal/handlers/timeline.go b/internal/handlers/timeline.go index 5e583d6..fa5bcec 100644 --- a/internal/handlers/timeline.go +++ b/internal/handlers/timeline.go @@ -21,6 +21,19 @@ type TimelineData struct { TodayLabel string // e.g., "Today - Monday" TomorrowLabel string // e.g., "Tomorrow - Tuesday" LaterLabel string // e.g., "Wednesday, Jan 29" + + // Calendar view bounds (1 hour before first event, 1 hour after last) + TodayStartHour int + TodayEndHour int + TodayHours []int // Slice of hours to render + + TomorrowStartHour int + TomorrowEndHour int + TomorrowHours []int + + // Current time for "now" line + NowHour int + NowMinute int } // HandleTimeline renders the timeline view @@ -70,6 +83,8 @@ func (h *Handler) HandleTimeline(w http.ResponseWriter, r *http.Request) { TodayLabel: "Today - " + now.Format("Monday"), TomorrowLabel: "Tomorrow - " + tomorrow.Format("Monday"), LaterLabel: dayAfterTomorrow.Format("Monday, Jan 2") + "+", + NowHour: now.Hour(), + NowMinute: now.Minute(), } for _, item := range items { switch item.DaySection { @@ -82,5 +97,77 @@ func (h *Handler) HandleTimeline(w http.ResponseWriter, r *http.Request) { } } + // Calculate calendar bounds for Today (1 hour buffer before/after timed events) + data.TodayStartHour, data.TodayEndHour = calcCalendarBounds(data.TodayItems, now.Hour()) + for h := data.TodayStartHour; h <= data.TodayEndHour; h++ { + data.TodayHours = append(data.TodayHours, h) + } + + // Calculate calendar bounds for Tomorrow + data.TomorrowStartHour, data.TomorrowEndHour = calcCalendarBounds(data.TomorrowItems, -1) + for h := data.TomorrowStartHour; h <= data.TomorrowEndHour; h++ { + data.TomorrowHours = append(data.TomorrowHours, h) + } + HTMLResponse(w, h.templates, "timeline-tab", data) } + +// calcCalendarBounds returns start/end hours for calendar view based on timed events. +// If currentHour >= 0, it's included in the range (for "now" line visibility). +// Returns hours clamped to 0-23 with 1-hour buffer before/after events. +func calcCalendarBounds(items []models.TimelineItem, currentHour int) (startHour, endHour int) { + minHour := 23 + maxHour := 0 + hasTimedEvents := false + + for _, item := range items { + // Skip all-day/overdue items (midnight with no real time) + if item.IsAllDay || item.IsOverdue { + continue + } + h := item.Time.Hour() + // Skip midnight items unless they have an end time + if h == 0 && item.Time.Minute() == 0 && item.EndTime == nil { + continue + } + hasTimedEvents = true + if h < minHour { + minHour = h + } + endH := h + if item.EndTime != nil { + endH = item.EndTime.Hour() + } + if endH > maxHour { + maxHour = endH + } + } + + // Include current hour if provided + if currentHour >= 0 { + hasTimedEvents = true + if currentHour < minHour { + minHour = currentHour + } + if currentHour > maxHour { + maxHour = currentHour + } + } + + if !hasTimedEvents { + // Default: show 8am-6pm + return 8, 18 + } + + // Add 1 hour buffer, clamp to valid range + startHour = minHour - 1 + if startHour < 0 { + startHour = 0 + } + endHour = maxHour + 1 + if endHour > 23 { + endHour = 23 + } + + return startHour, endHour +} -- cgit v1.2.3