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