summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-02-01 10:52:28 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-02-01 10:52:28 -1000
commit1c6552117038cb7c01e016dbf1ac062e1d9f9c73 (patch)
treeff65c67a40e08a14f89fe3057a8ac4886d94b75b /internal/handlers
parente0e0dc11195c0e0516b45975de51df1dc98f83de (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/handlers.go7
-rw-r--r--internal/handlers/handlers_test.go9
-rw-r--r--internal/handlers/timeline.go87
3 files changed, 100 insertions, 3 deletions
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
+}