449 lines
12 KiB
Go
449 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DailyNoteInfo contient les métadonnées d'une daily note
|
|
type DailyNoteInfo struct {
|
|
Date time.Time
|
|
Path string
|
|
Exists bool
|
|
Title string
|
|
DayOfWeek string
|
|
DayOfMonth int
|
|
}
|
|
|
|
// CalendarDay représente un jour dans le calendrier
|
|
type CalendarDay struct {
|
|
Day int
|
|
Date time.Time
|
|
HasNote bool
|
|
IsToday bool
|
|
NotePath string
|
|
InMonth bool // Indique si le jour appartient au mois affiché
|
|
}
|
|
|
|
// CalendarData contient les données pour le template du calendrier
|
|
type CalendarData struct {
|
|
Year int
|
|
Month time.Month
|
|
MonthName string
|
|
Weeks [][7]CalendarDay
|
|
PrevMonth string // Format: YYYY-MM
|
|
NextMonth string // Format: YYYY-MM
|
|
CurrentMonth string // Format: YYYY-MM
|
|
}
|
|
|
|
// getDailyNotePath retourne le chemin d'une daily note pour une date donnée
|
|
// Format: notes/daily/2025/01/11.md
|
|
func (h *Handler) getDailyNotePath(date time.Time) string {
|
|
year := date.Format("2006")
|
|
month := date.Format("01")
|
|
day := date.Format("02")
|
|
|
|
relativePath := filepath.Join("daily", year, month, fmt.Sprintf("%s.md", day))
|
|
return relativePath
|
|
}
|
|
|
|
// getDailyNoteAbsolutePath retourne le chemin absolu d'une daily note
|
|
func (h *Handler) getDailyNoteAbsolutePath(date time.Time) string {
|
|
relativePath := h.getDailyNotePath(date)
|
|
return filepath.Join(h.notesDir, relativePath)
|
|
}
|
|
|
|
// dailyNoteExists vérifie si une daily note existe pour une date donnée
|
|
func (h *Handler) dailyNoteExists(date time.Time) bool {
|
|
absPath := h.getDailyNoteAbsolutePath(date)
|
|
_, err := os.Stat(absPath)
|
|
return err == nil
|
|
}
|
|
|
|
// createDailyNote crée une daily note avec un template par défaut
|
|
func (h *Handler) createDailyNote(date time.Time) error {
|
|
absPath := h.getDailyNoteAbsolutePath(date)
|
|
|
|
// Créer les dossiers parents si nécessaire
|
|
dir := filepath.Dir(absPath)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("impossible de créer les dossiers: %w", err)
|
|
}
|
|
|
|
// Vérifier si le fichier existe déjà
|
|
if _, err := os.Stat(absPath); err == nil {
|
|
return nil // Fichier existe déjà, ne pas écraser
|
|
}
|
|
|
|
// Formatter les dates
|
|
dateStr := date.Format("02-01-2006")
|
|
dateTimeStr := date.Format("02-01-2006:15:04")
|
|
|
|
// Noms des jours en français
|
|
dayNames := map[time.Weekday]string{
|
|
time.Monday: "Lundi",
|
|
time.Tuesday: "Mardi",
|
|
time.Wednesday: "Mercredi",
|
|
time.Thursday: "Jeudi",
|
|
time.Friday: "Vendredi",
|
|
time.Saturday: "Samedi",
|
|
time.Sunday: "Dimanche",
|
|
}
|
|
|
|
// Noms des mois en français
|
|
monthNames := map[time.Month]string{
|
|
time.January: "janvier",
|
|
time.February: "février",
|
|
time.March: "mars",
|
|
time.April: "avril",
|
|
time.May: "mai",
|
|
time.June: "juin",
|
|
time.July: "juillet",
|
|
time.August: "août",
|
|
time.September: "septembre",
|
|
time.October: "octobre",
|
|
time.November: "novembre",
|
|
time.December: "décembre",
|
|
}
|
|
|
|
dayName := dayNames[date.Weekday()]
|
|
monthName := monthNames[date.Month()]
|
|
|
|
// Template de la daily note
|
|
template := fmt.Sprintf(`---
|
|
title: "Daily Note - %s"
|
|
date: "%s"
|
|
last_modified: "%s"
|
|
tags: [daily]
|
|
---
|
|
|
|
# 📅 %s %d %s %d
|
|
|
|
## 🎯 Objectifs du jour
|
|
-
|
|
|
|
## 📝 Notes
|
|
-
|
|
|
|
## ✅ Accompli
|
|
-
|
|
|
|
## 💭 Réflexions
|
|
-
|
|
|
|
## 🔗 Liens
|
|
-
|
|
`, date.Format("2006-01-02"), dateStr, dateTimeStr, dayName, date.Day(), monthName, date.Year())
|
|
|
|
// Écrire le fichier
|
|
if err := os.WriteFile(absPath, []byte(template), 0644); err != nil {
|
|
return fmt.Errorf("impossible d'écrire le fichier: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleDailyToday gère GET /api/daily/today - Ouvre ou crée la note du jour
|
|
func (h *Handler) handleDailyToday(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
today := time.Now()
|
|
|
|
// Créer la note si elle n'existe pas
|
|
if !h.dailyNoteExists(today) {
|
|
if err := h.createDailyNote(today); err != nil {
|
|
h.logger.Printf("Erreur création daily note: %v", err)
|
|
http.Error(w, "Erreur lors de la création de la note", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Déclencher la ré-indexation
|
|
go h.idx.Load(h.notesDir)
|
|
}
|
|
|
|
// Rediriger vers l'endpoint normal de note
|
|
notePath := h.getDailyNotePath(today)
|
|
http.Redirect(w, r, "/api/notes/"+notePath, http.StatusSeeOther)
|
|
}
|
|
|
|
// handleDailyDate gère GET /api/daily/{YYYY-MM-DD} - Ouvre ou crée la note d'une date spécifique
|
|
func (h *Handler) handleDailyDate(w http.ResponseWriter, r *http.Request, dateStr string) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Parser la date (format YYYY-MM-DD)
|
|
date, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
http.Error(w, "Format de date invalide (attendu: YYYY-MM-DD)", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Créer la note si elle n'existe pas
|
|
if !h.dailyNoteExists(date) {
|
|
if err := h.createDailyNote(date); err != nil {
|
|
h.logger.Printf("Erreur création daily note: %v", err)
|
|
http.Error(w, "Erreur lors de la création de la note", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Déclencher la ré-indexation
|
|
go h.idx.Load(h.notesDir)
|
|
}
|
|
|
|
// Rediriger vers l'endpoint normal de note
|
|
notePath := h.getDailyNotePath(date)
|
|
http.Redirect(w, r, "/api/notes/"+notePath, http.StatusSeeOther)
|
|
}
|
|
|
|
// handleDailyCalendar gère GET /api/daily/calendar/{YYYY}/{MM} - Retourne le HTML du calendrier
|
|
func (h *Handler) handleDailyCalendar(w http.ResponseWriter, r *http.Request, yearStr, monthStr string) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Si ce n'est pas une requête HTMX, rediriger vers la page principale
|
|
if r.Header.Get("HX-Request") == "" {
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Parser année et mois
|
|
year, err := strconv.Atoi(yearStr)
|
|
if err != nil || year < 1900 || year > 2100 {
|
|
http.Error(w, "Année invalide", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
month, err := strconv.Atoi(monthStr)
|
|
if err != nil || month < 1 || month > 12 {
|
|
http.Error(w, "Mois invalide", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Créer les données du calendrier
|
|
calendarData := h.buildCalendarData(year, time.Month(month))
|
|
|
|
// Rendre le template
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := h.templates.ExecuteTemplate(w, "daily-calendar.html", calendarData); err != nil {
|
|
h.logger.Printf("Erreur template calendrier: %v", err)
|
|
http.Error(w, "Erreur de rendu", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// buildCalendarData construit les données du calendrier pour un mois donné
|
|
func (h *Handler) buildCalendarData(year int, month time.Month) *CalendarData {
|
|
// Premier jour du mois
|
|
firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
|
|
|
|
// Dernier jour du mois
|
|
lastDay := firstDay.AddDate(0, 1, -1)
|
|
|
|
// Date d'aujourd'hui
|
|
today := time.Now()
|
|
|
|
// Noms des mois en français
|
|
monthNames := map[time.Month]string{
|
|
time.January: "Janvier",
|
|
time.February: "Février",
|
|
time.March: "Mars",
|
|
time.April: "Avril",
|
|
time.May: "Mai",
|
|
time.June: "Juin",
|
|
time.July: "Juillet",
|
|
time.August: "Août",
|
|
time.September: "Septembre",
|
|
time.October: "Octobre",
|
|
time.November: "Novembre",
|
|
time.December: "Décembre",
|
|
}
|
|
|
|
data := &CalendarData{
|
|
Year: year,
|
|
Month: month,
|
|
MonthName: monthNames[month],
|
|
Weeks: make([][7]CalendarDay, 0),
|
|
}
|
|
|
|
// Calculer mois précédent et suivant
|
|
prevMonth := firstDay.AddDate(0, -1, 0)
|
|
nextMonth := firstDay.AddDate(0, 1, 0)
|
|
data.PrevMonth = fmt.Sprintf("%d-%02d", prevMonth.Year(), prevMonth.Month())
|
|
data.NextMonth = fmt.Sprintf("%d-%02d", nextMonth.Year(), nextMonth.Month())
|
|
data.CurrentMonth = fmt.Sprintf("%d-%02d", year, month)
|
|
|
|
// Construire les semaines
|
|
// Lundi = 0, Dimanche = 6
|
|
var week [7]CalendarDay
|
|
weekDay := 0
|
|
|
|
// Jour de la semaine du premier jour (convertir : Dimanche=0 → Lundi=0)
|
|
firstWeekday := int(firstDay.Weekday())
|
|
if firstWeekday == 0 {
|
|
firstWeekday = 7 // Dimanche devient 7
|
|
}
|
|
firstWeekday-- // Maintenant Lundi=0
|
|
|
|
// Remplir les jours avant le premier du mois (mois précédent)
|
|
prevMonthLastDay := firstDay.AddDate(0, 0, -1)
|
|
for i := 0; i < firstWeekday; i++ {
|
|
daysBack := firstWeekday - i
|
|
date := prevMonthLastDay.AddDate(0, 0, -daysBack+1)
|
|
week[i] = CalendarDay{
|
|
Day: date.Day(),
|
|
Date: date,
|
|
HasNote: h.dailyNoteExists(date),
|
|
IsToday: isSameDay(date, today),
|
|
InMonth: false,
|
|
}
|
|
}
|
|
weekDay = firstWeekday
|
|
|
|
// Remplir les jours du mois
|
|
for day := 1; day <= lastDay.Day(); day++ {
|
|
date := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
|
|
|
|
week[weekDay] = CalendarDay{
|
|
Day: day,
|
|
Date: date,
|
|
HasNote: h.dailyNoteExists(date),
|
|
IsToday: isSameDay(date, today),
|
|
NotePath: h.getDailyNotePath(date),
|
|
InMonth: true,
|
|
}
|
|
|
|
weekDay++
|
|
if weekDay == 7 {
|
|
data.Weeks = append(data.Weeks, week)
|
|
week = [7]CalendarDay{}
|
|
weekDay = 0
|
|
}
|
|
}
|
|
|
|
// Remplir les jours après le dernier du mois (mois suivant)
|
|
if weekDay > 0 {
|
|
nextMonthDay := 1
|
|
for weekDay < 7 {
|
|
date := time.Date(year, month+1, nextMonthDay, 0, 0, 0, 0, time.Local)
|
|
week[weekDay] = CalendarDay{
|
|
Day: nextMonthDay,
|
|
Date: date,
|
|
HasNote: h.dailyNoteExists(date),
|
|
IsToday: isSameDay(date, today),
|
|
InMonth: false,
|
|
}
|
|
weekDay++
|
|
nextMonthDay++
|
|
}
|
|
data.Weeks = append(data.Weeks, week)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// handleDailyRecent gère GET /api/daily/recent - Retourne les 7 dernières daily notes
|
|
func (h *Handler) handleDailyRecent(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Si ce n'est pas une requête HTMX, rediriger vers la page principale
|
|
if r.Header.Get("HX-Request") == "" {
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Chercher les daily notes des 14 derniers jours (au cas où certaines manquent)
|
|
recentNotes := make([]*DailyNoteInfo, 0, 7)
|
|
|
|
today := time.Now()
|
|
for i := 0; i < 14 && len(recentNotes) < 7; i++ {
|
|
date := today.AddDate(0, 0, -i)
|
|
|
|
if h.dailyNoteExists(date) {
|
|
dayNames := map[time.Weekday]string{
|
|
time.Monday: "Lun",
|
|
time.Tuesday: "Mar",
|
|
time.Wednesday: "Mer",
|
|
time.Thursday: "Jeu",
|
|
time.Friday: "Ven",
|
|
time.Saturday: "Sam",
|
|
time.Sunday: "Dim",
|
|
}
|
|
|
|
info := &DailyNoteInfo{
|
|
Date: date,
|
|
Path: h.getDailyNotePath(date),
|
|
Exists: true,
|
|
Title: date.Format("02/01/2006"),
|
|
DayOfWeek: dayNames[date.Weekday()],
|
|
DayOfMonth: date.Day(),
|
|
}
|
|
recentNotes = append(recentNotes, info)
|
|
}
|
|
}
|
|
|
|
// Rendre le template
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := h.templates.ExecuteTemplate(w, "daily-recent.html", map[string]interface{}{
|
|
"Notes": recentNotes,
|
|
}); err != nil {
|
|
h.logger.Printf("Erreur template notes récentes: %v", err)
|
|
http.Error(w, "Erreur de rendu", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// isSameDay vérifie si deux dates sont le même jour
|
|
func isSameDay(d1, d2 time.Time) bool {
|
|
y1, m1, day1 := d1.Date()
|
|
y2, m2, day2 := d2.Date()
|
|
return y1 == y2 && m1 == m2 && day1 == day2
|
|
}
|
|
|
|
// handleDaily route les requêtes /api/daily/*
|
|
func (h *Handler) handleDaily(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/daily")
|
|
path = strings.TrimPrefix(path, "/")
|
|
|
|
// /api/daily/today
|
|
if path == "today" || path == "" {
|
|
h.handleDailyToday(w, r)
|
|
return
|
|
}
|
|
|
|
// /api/daily/recent
|
|
if path == "recent" {
|
|
h.handleDailyRecent(w, r)
|
|
return
|
|
}
|
|
|
|
// /api/daily/calendar/{YYYY}/{MM}
|
|
if strings.HasPrefix(path, "calendar/") {
|
|
parts := strings.Split(strings.TrimPrefix(path, "calendar/"), "/")
|
|
if len(parts) == 2 {
|
|
h.handleDailyCalendar(w, r, parts[0], parts[1])
|
|
return
|
|
}
|
|
}
|
|
|
|
// /api/daily/{YYYY-MM-DD}
|
|
if len(path) == 10 && path[4] == '-' && path[7] == '-' {
|
|
h.handleDailyDate(w, r, path)
|
|
return
|
|
}
|
|
|
|
http.NotFound(w, r)
|
|
}
|