package web import ( "errors" "net/http" "strconv" "strings" "github.com/gramanas/blogspace/internal/markdown" "github.com/gramanas/blogspace/internal/store" ) // Announcements ("sections" in the schema) are notices shown on every page and post. const maxSectionBytes = 20_000 func (s *Server) sectionsURL(r *http.Request) string { return "/b/" + currentBlog(r).Subdomain + "/announcements" } func (s *Server) handleSections(w http.ResponseWriter, r *http.Request) { secs, err := blogStore(r).ListSections(r.Context()) if err != nil { s.serverError(w, err) return } s.render(w, r, "dashboard/sections.html", map[string]any{"sections": secs}) } func (s *Server) loadSection(w http.ResponseWriter, r *http.Request) *store.Section { id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) sec, err := blogStore(r).SectionByID(r.Context(), id) if err != nil { if errors.Is(err, store.ErrNotFound) { http.NotFound(w, r) } else { s.serverError(w, err) } return nil } return sec } func (s *Server) handleSectionForm(w http.ResponseWriter, r *http.Request) { sec := &store.Section{Placement: "main-top", Style: "note", Enabled: true} if r.PathValue("id") != "" { if sec = s.loadSection(w, r); sec == nil { return } } s.render(w, r, "dashboard/section_form.html", map[string]any{"section": sec}) } // normalizeSection clamps the enum fields so templates and CSS class names can trust them. func normalizeSection(sec *store.Section) { sec.Placement = pick(sec.Column(), "main", "left", "right") + "-" + pick(sec.Position(), "top", "bottom") sec.Style = pick(sec.Style, "note", "plain", "warning") } func (s *Server) handleSectionSave(w http.ResponseWriter, r *http.Request) { sec := &store.Section{} if r.PathValue("id") != "" { if sec = s.loadSection(w, r); sec == nil { return } } sec.Title = strings.TrimSpace(r.FormValue("title")) sec.BodyMD = strings.ReplaceAll(r.FormValue("body"), "\r\n", "\n") sec.Placement = r.FormValue("column") + "-" + r.FormValue("position") sec.Style = r.FormValue("style") sec.Enabled = r.FormValue("enabled") == "on" normalizeSection(sec) fail := func(msg string) { s.renderStatus(w, r, http.StatusBadRequest, "dashboard/section_form.html", map[string]any{"section": sec, "error": msg}) } img, err := s.readUpload(r, "inline_image") switch { case err != nil: fail("Image not added: " + err.Error()) return case len(sec.Title) > 120: fail("Title is too long (max 120 characters).") return case strings.TrimSpace(sec.BodyMD) == "" && img == nil: fail("Write some text for the announcement.") return case len(sec.BodyMD) > maxSectionBytes: fail("Announcement is too long (20 KB max).") return } sec.BodyMD = appendImageMD(sec.BodyMD, img) sec.BodyHTML = markdown.Render(sec.BodyMD) if sec.ID == 0 { sec, err = blogStore(r).CreateSection(r.Context(), sec) } else { err = blogStore(r).UpdateSection(r.Context(), sec) } if err != nil { s.serverError(w, err) return } redirectOK(w, r, s.sectionsURL(r)+"/"+strconv.FormatInt(sec.ID, 10)+"/edit", "Saved. Refresh your blog to see it.") } func (s *Server) handleSectionDeleteConfirm(w http.ResponseWriter, r *http.Request) { sec := s.loadSection(w, r) if sec == nil { return } what := "this announcement" if sec.Title != "" { what = "the announcement \"" + sec.Title + "\"" } s.render(w, r, "dashboard/confirm.html", map[string]any{"what": what, "action": r.URL.Path, "back": s.sectionsURL(r)}) } func (s *Server) handleSectionDelete(w http.ResponseWriter, r *http.Request) { sec := s.loadSection(w, r) if sec == nil { return } if err := blogStore(r).DeleteSection(r.Context(), sec.ID); err != nil { s.serverError(w, err) return } redirectOK(w, r, s.sectionsURL(r), "Announcement deleted.") } func (s *Server) handleSectionMove(w http.ResponseWriter, r *http.Request) { sec := s.loadSection(w, r) if sec == nil { return } dir := 1 if r.FormValue("dir") == "up" { dir = -1 } if err := blogStore(r).MoveSection(r.Context(), sec.ID, dir); err != nil { s.serverError(w, err) return } http.Redirect(w, r, s.sectionsURL(r), http.StatusSeeOther) } // handleSectionToggle flips visibility without a trip through the edit form. func (s *Server) handleSectionToggle(w http.ResponseWriter, r *http.Request) { sec := s.loadSection(w, r) if sec == nil { return } if err := blogStore(r).SetSectionEnabled(r.Context(), sec.ID, !sec.Enabled); err != nil { s.serverError(w, err) return } msg := "Announcement hidden." if !sec.Enabled { msg = "Announcement shown. Refresh your blog to see it." } redirectOK(w, r, s.sectionsURL(r), msg) }