Initial implementation
This commit is contained in:
357
internal/handlers/handlers.go
Normal file
357
internal/handlers/handlers.go
Normal file
@@ -0,0 +1,357 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user