package web import ( "embed" "fmt" "html/template" "log/slog" "net/http" "strings" "time" "git.vakhrushev.me/av/remembos/internal/memory" ) //go:embed templates/*.html var templateFS embed.FS var templates = template.Must(template.ParseFS(templateFS, "templates/*.html")) type imageData struct { URL string Alt string } type memoryData struct { DateFormatted string AgoText string Content string Tags []string Images []imageData MemoURL string Tier int ShowCount int AllowLoadMore bool } type errorData struct { Message string } type Handler struct { service *memory.Service logger *slog.Logger mux *http.ServeMux memosURL string // internal Memos URL (for attachment files) publicURL string // public Memos URL (for memo links) allowLoadMore bool } func NewHandler(service *memory.Service, memosURL, publicURL string, allowLoadMore bool, logger *slog.Logger) *Handler { pub := publicURL if pub == "" { pub = memosURL } h := &Handler{ service: service, memosURL: strings.TrimRight(memosURL, "/"), publicURL: strings.TrimRight(pub, "/"), allowLoadMore: allowLoadMore, logger: logger, mux: http.NewServeMux(), } h.mux.HandleFunc("GET /", h.handleMemory) h.mux.HandleFunc("GET /health", h.handleHealth) h.mux.HandleFunc("POST /more", h.handleLoadMore) return h } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } mem, err := h.service.GetTodayMemory(r.Context()) if err != nil { h.logger.Error("failed to get memory", "error", err) w.WriteHeader(http.StatusInternalServerError) if err := templates.ExecuteTemplate(w, "error.html", errorData{ Message: "Не удалось загрузить воспоминание", }); err != nil { h.logger.Error("template render failed", "error", err) } return } if mem == nil { w.WriteHeader(http.StatusOK) if err := templates.ExecuteTemplate(w, "error.html", errorData{ Message: "Нет заметок для воспоминания", }); err != nil { h.logger.Error("template render failed", "error", err) } return } images := make([]imageData, 0, len(mem.Memo.Attachments)) for _, att := range mem.Memo.Attachments { if !att.IsImage() { continue } var imgURL string if att.ExternalLink != "" { imgURL = att.ExternalLink } else { imgURL = fmt.Sprintf("%s/file/%s/%s", h.memosURL, att.Name, att.Filename) } images = append(images, imageData{URL: imgURL, Alt: att.Filename}) } // Link to original memo: {publicURL}/{memoName} memoURL := fmt.Sprintf("%s/%s", h.publicURL, mem.Memo.Name) data := memoryData{ DateFormatted: formatDate(mem.Date), AgoText: agoText(mem.Date), Content: mem.Memo.Content, Tags: mem.Memo.Tags, Images: images, MemoURL: memoURL, Tier: mem.Tier, ShowCount: mem.ShowCount, AllowLoadMore: h.allowLoadMore, } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := templates.ExecuteTemplate(w, "memory.html", data); err != nil { h.logger.Error("template render failed", "error", err) } } func (h *Handler) handleHealth(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "ok") } func (h *Handler) handleLoadMore(w http.ResponseWriter, r *http.Request) { if !h.allowLoadMore { http.Error(w, "load more is disabled", http.StatusForbidden) return } _, err := h.service.LoadNewMemory(r.Context()) if err != nil { h.logger.Error("failed to load new memory", "error", err) } http.Redirect(w, r, "/", http.StatusSeeOther) } var months = [...]string{ 1: "января", 2: "февраля", 3: "марта", 4: "апреля", 5: "мая", 6: "июня", 7: "июля", 8: "августа", 9: "сентября", 10: "октября", 11: "ноября", 12: "декабря", } func formatDate(t time.Time) string { return fmt.Sprintf("%d %s %d", t.Day(), months[t.Month()], t.Year()) } func agoText(t time.Time) string { now := time.Now() years := now.Year() - t.Year() monthsDiff := int(now.Month()) - int(t.Month()) if monthsDiff < 0 { years-- monthsDiff += 12 } if years > 0 { return fmt.Sprintf("%s назад", pluralYears(years)) } if monthsDiff > 0 { return fmt.Sprintf("%s назад", pluralMonths(monthsDiff)) } return "" } func pluralYears(n int) string { mod10 := n % 10 mod100 := n % 100 switch { case mod100 >= 11 && mod100 <= 14: return fmt.Sprintf("%d лет", n) case mod10 == 1: return fmt.Sprintf("%d год", n) case mod10 >= 2 && mod10 <= 4: return fmt.Sprintf("%d года", n) default: return fmt.Sprintf("%d лет", n) } } func pluralMonths(n int) string { mod10 := n % 10 mod100 := n % 100 switch { case mod100 >= 11 && mod100 <= 14: return fmt.Sprintf("%d месяцев", n) case mod10 == 1: return fmt.Sprintf("%d месяц", n) case mod10 >= 2 && mod10 <= 4: return fmt.Sprintf("%d месяца", n) default: return fmt.Sprintf("%d месяцев", n) } }