// Package handlers contains HTTP handlers for all routes. package handlers import ( "context" "html/template" "net/http" "strconv" "time" "github.com/luxick/chronological/internal/models" "github.com/luxick/chronological/internal/storage" ) // Handlers holds all HTTP handlers and their dependencies. type Handlers struct { templates *template.Template icsStore *storage.ICSStore photoStore *storage.PhotoStore caldavStore *storage.CalDAVStore } // New creates a new Handlers instance. func New(tmpl *template.Template, ics *storage.ICSStore, photos *storage.PhotoStore, caldav *storage.CalDAVStore) *Handlers { return &Handlers{ templates: tmpl, icsStore: ics, photoStore: photos, caldavStore: caldav, } } // HandleIndex serves the main timeline page. func (h *Handlers) HandleIndex(w http.ResponseWriter, r *http.Request) { now := time.Now() data := h.buildPageData(now.Year(), now.Month()) h.render(w, "index.html", data) } // HandleCalendar serves the calendar grid partial for HTMX. func (h *Handlers) HandleCalendar(w http.ResponseWriter, r *http.Request) { year, month, err := parseYearMonth(r) if err != nil { http.Error(w, "Invalid date", http.StatusBadRequest) return } data := h.buildCalendarData(year, month) h.render(w, "calendar.html", data) } // HandleTimeline serves the timeline partial for HTMX. func (h *Handlers) HandleTimeline(w http.ResponseWriter, r *http.Request) { year, month, err := parseYearMonth(r) if err != nil { http.Error(w, "Invalid date", http.StatusBadRequest) return } data := h.buildTimelineData(year, month) h.render(w, "timeline.html", data) } // HandleDay serves the day detail view. func (h *Handlers) HandleDay(w http.ResponseWriter, r *http.Request) { year, month, err := parseYearMonth(r) if err != nil { http.Error(w, "Invalid date", http.StatusBadRequest) return } dayStr := r.PathValue("day") day, err := strconv.Atoi(dayStr) if err != nil || day < 1 || day > 31 { http.Error(w, "Invalid day", http.StatusBadRequest) return } date := time.Date(year, month, day, 0, 0, 0, 0, time.Local) data := h.buildDayData(date) h.render(w, "day.html", data) } // HandleGetDiary serves a diary entry for a specific date. func (h *Handlers) HandleGetDiary(w http.ResponseWriter, r *http.Request) { date, err := parseDate(r.PathValue("date")) if err != nil { http.Error(w, "Invalid date", http.StatusBadRequest) return } entry, err := h.icsStore.LoadDiaryEntry(date) if err != nil { http.Error(w, "Failed to load diary", http.StatusInternalServerError) return } data := map[string]any{ "Date": date, "Entry": entry, } h.render(w, "diary_form.html", data) } // HandleSaveDiary saves or updates a diary entry. func (h *Handlers) HandleSaveDiary(w http.ResponseWriter, r *http.Request) { date, err := parseDate(r.PathValue("date")) if err != nil { http.Error(w, "Invalid date", http.StatusBadRequest) return } if err := r.ParseForm(); err != nil { http.Error(w, "Failed to parse form", http.StatusBadRequest) return } text := r.FormValue("text") entry := &models.DiaryEntry{ Date: date, Text: text, } if err := h.icsStore.SaveDiaryEntry(entry); err != nil { http.Error(w, "Failed to save diary", http.StatusInternalServerError) return } // Return updated day content data := h.buildDayData(date) h.render(w, "day_content.html", data) } // HandleDeleteDiary deletes a diary entry. func (h *Handlers) HandleDeleteDiary(w http.ResponseWriter, r *http.Request) { date, err := parseDate(r.PathValue("date")) if err != nil { http.Error(w, "Invalid date", http.StatusBadRequest) return } if err := h.icsStore.DeleteDiaryEntry(date); err != nil { http.Error(w, "Failed to delete diary", http.StatusInternalServerError) return } // Return updated day content data := h.buildDayData(date) h.render(w, "day_content.html", data) } func (h *Handlers) render(w http.ResponseWriter, name string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := h.templates.ExecuteTemplate(w, name, data); err != nil { http.Error(w, "Template error", http.StatusInternalServerError) } } func (h *Handlers) buildPageData(year int, month time.Month) map[string]any { return map[string]any{ "Year": year, "Month": month, "Calendar": h.buildCalendarData(year, month), "Timeline": h.buildTimelineData(year, month), } } func (h *Handlers) buildCalendarData(year int, month time.Month) map[string]any { // Load content indicators for each day events, _ := h.icsStore.LoadEventsForMonth(year, month) diaries, _ := h.icsStore.LoadDiaryEntries() photos, _ := h.photoStore.LoadPhotosForMonth(year, month) // Load CalDAV events if available if h.caldavStore != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() caldavEvents, err := h.caldavStore.LoadEventsForMonth(ctx, year, month) if err == nil { events = append(events, caldavEvents...) } } // Build day content map days := make(map[int]*models.DayContent) daysInMonth := time.Date(year, month+1, 0, 0, 0, 0, 0, time.Local).Day() for day := 1; day <= daysInMonth; day++ { date := time.Date(year, month, day, 0, 0, 0, 0, time.Local) content := &models.DayContent{Date: date} // Add events for this day for _, event := range events { if event.IsOnDate(date) { content.Events = append(content.Events, event) } } // Add diary entry for this day for _, diary := range diaries { if diary.Date.Truncate(24 * time.Hour).Equal(date.Truncate(24 * time.Hour)) { content.Diary = diary break } } // Add photos for this day for _, photo := range photos { if photo.Date.Truncate(24 * time.Hour).Equal(date.Truncate(24 * time.Hour)) { content.Photos = append(content.Photos, photo) } } days[day] = content } // Calculate first day offset (0 = Sunday) firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local) firstDayWeekday := int(firstDay.Weekday()) // Previous/next month prevMonth := time.Date(year, month-1, 1, 0, 0, 0, 0, time.Local) nextMonth := time.Date(year, month+1, 1, 0, 0, 0, 0, time.Local) return map[string]any{ "Year": year, "Month": month, "MonthName": month.String(), "DaysInMonth": daysInMonth, "FirstDayWeekday": firstDayWeekday, "Days": days, "PrevYear": prevMonth.Year(), "PrevMonth": int(prevMonth.Month()), "NextYear": nextMonth.Year(), "NextMonth": int(nextMonth.Month()), } } func (h *Handlers) buildTimelineData(year int, month time.Month) map[string]any { events, _ := h.icsStore.LoadEventsForMonth(year, month) diaries, _ := h.icsStore.LoadDiaryEntries() photos, _ := h.photoStore.LoadPhotosForMonth(year, month) // Load CalDAV events if available if h.caldavStore != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() caldavEvents, err := h.caldavStore.LoadEventsForMonth(ctx, year, month) if err == nil { events = append(events, caldavEvents...) } } // Build entries grouped by day daysInMonth := time.Date(year, month+1, 0, 0, 0, 0, 0, time.Local).Day() var daysWithContent []*models.DayContent for day := 1; day <= daysInMonth; day++ { date := time.Date(year, month, day, 0, 0, 0, 0, time.Local) content := &models.DayContent{Date: date} for _, event := range events { if event.IsOnDate(date) { content.Events = append(content.Events, event) } } for _, diary := range diaries { if diary.Date.Truncate(24 * time.Hour).Equal(date.Truncate(24 * time.Hour)) { content.Diary = diary break } } for _, photo := range photos { if photo.Date.Truncate(24 * time.Hour).Equal(date.Truncate(24 * time.Hour)) { content.Photos = append(content.Photos, photo) } } if content.HasContent() { daysWithContent = append(daysWithContent, content) } } // Previous/next month prevMonth := time.Date(year, month-1, 1, 0, 0, 0, 0, time.Local) nextMonth := time.Date(year, month+1, 1, 0, 0, 0, 0, time.Local) return map[string]any{ "Year": year, "Month": month, "MonthName": month.String(), "Days": daysWithContent, "PrevYear": prevMonth.Year(), "PrevMonth": int(prevMonth.Month()), "NextYear": nextMonth.Year(), "NextMonth": int(nextMonth.Month()), } } func (h *Handlers) buildDayData(date time.Time) map[string]any { content := &models.DayContent{Date: date} events, _ := h.icsStore.LoadEventsForMonth(date.Year(), date.Month()) for _, event := range events { if event.IsOnDate(date) { content.Events = append(content.Events, event) } } // Load CalDAV events if available if h.caldavStore != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() caldavEvents, err := h.caldavStore.LoadEventsForMonth(ctx, date.Year(), date.Month()) if err == nil { for _, event := range caldavEvents { if event.IsOnDate(date) { content.Events = append(content.Events, event) } } } } entry, _ := h.icsStore.LoadDiaryEntry(date) content.Diary = entry photos, _ := h.photoStore.LoadPhotosForDate(date) content.Photos = photos return map[string]any{ "Date": date, "Content": content, } } func parseYearMonth(r *http.Request) (int, time.Month, error) { yearStr := r.PathValue("year") monthStr := r.PathValue("month") year, err := strconv.Atoi(yearStr) if err != nil { return 0, 0, err } monthInt, err := strconv.Atoi(monthStr) if err != nil || monthInt < 1 || monthInt > 12 { return 0, 0, err } return year, time.Month(monthInt), nil } func parseDate(dateStr string) (time.Time, error) { return time.Parse("2006-01-02", dateStr) }