358 lines
9.6 KiB
Go
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)
|
|
}
|