Files
chronological/internal/handlers/handlers.go
2025-12-10 11:10:52 +01:00

358 lines
9.6 KiB
Go

// 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)
}