diff options
Diffstat (limited to 'internal/web/handlers_pages.go')
| -rw-r--r-- | internal/web/handlers_pages.go | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/internal/web/handlers_pages.go b/internal/web/handlers_pages.go new file mode 100644 index 0000000..1b79280 --- /dev/null +++ b/internal/web/handlers_pages.go @@ -0,0 +1,161 @@ +package web + +import ( + "errors" + "net/http" + "strconv" + "strings" + + "github.com/gramanas/blogspace/internal/markdown" + "github.com/gramanas/blogspace/internal/slug" + "github.com/gramanas/blogspace/internal/store" +) + +// Page slugs that would collide with blog routes. +var reservedPageSlugs = map[string]bool{"media": true, "static": true, "feed.xml": true} + +func (s *Server) handlePages(w http.ResponseWriter, r *http.Request) { + pages, err := s.st.ListPages(r.Context(), currentBlog(r).ID) + if err != nil { + s.serverError(w, err) + return + } + s.render(w, r, "dashboard/pages.html", map[string]any{"pages": pages}) +} + +// loadPage fetches the page named in the URL, or nil (having written the response) on failure. +func (s *Server) loadPage(w http.ResponseWriter, r *http.Request) *store.Page { + id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64) + p, err := s.st.PageByID(r.Context(), currentBlog(r).ID, id) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + http.NotFound(w, r) + } else { + s.serverError(w, err) + } + return nil + } + return p +} + +func (s *Server) handlePageForm(w http.ResponseWriter, r *http.Request) { + p := &store.Page{ShowInNav: true} + if r.PathValue("id") != "" { + if p = s.loadPage(w, r); p == nil { + return + } + } + s.render(w, r, "dashboard/page_form.html", map[string]any{"page": p}) +} + +func (s *Server) handlePageSave(w http.ResponseWriter, r *http.Request) { + blog := currentBlog(r) + p := &store.Page{BlogID: blog.ID} + if r.PathValue("id") != "" { + if p = s.loadPage(w, r); p == nil { + return + } + } + p.Title = strings.TrimSpace(r.FormValue("title")) + p.Slug = strings.TrimSpace(r.FormValue("slug")) + p.IntroMD = r.FormValue("intro") + p.ShowInNav = r.FormValue("show_in_nav") == "on" + autoSlug := p.Slug == "" + if autoSlug { + p.Slug = slug.Make(p.Title) + } + var msg string + switch { + case p.Title == "" || len(p.Title) > 120: + msg = "Title is required (max 120 characters)." + case !slug.Valid(p.Slug) || reservedPageSlugs[p.Slug]: + msg = "Slug may only contain lowercase letters, digits and dashes (and not be a reserved word)." + } + if msg != "" { + s.renderStatus(w, r, http.StatusBadRequest, "dashboard/page_form.html", map[string]any{"page": p, "error": msg}) + return + } + p.IntroHTML = markdown.Render(p.IntroMD) + var err error + base := p.Slug + for n := 1; ; n++ { // generated slugs get -2, -3… on collision; typed ones report the conflict + p.Slug = slug.WithSuffix(base, n) + if p.ID == 0 { + var created *store.Page + if created, err = s.st.CreatePage(r.Context(), p); err == nil { + p = created + } + } else { + err = s.st.UpdatePage(r.Context(), p) + } + if !errors.Is(err, store.ErrConflict) || !autoSlug || n >= 50 { + break + } + } + if err != nil { + if errors.Is(err, store.ErrConflict) { + s.renderStatus(w, r, http.StatusConflict, "dashboard/page_form.html", map[string]any{"page": p, "error": "A page with that slug already exists."}) + return + } + s.serverError(w, err) + return + } + redirectOK(w, r, "/b/"+blog.Subdomain+"/pages/"+strconv.FormatInt(p.ID, 10)+"/edit", "Saved. Refresh your blog to see it.") +} + +func (s *Server) handlePageDeleteConfirm(w http.ResponseWriter, r *http.Request) { + p := s.loadPage(w, r) + if p == nil { + return + } + s.render(w, r, "dashboard/confirm.html", map[string]any{ + "what": "the page \"" + p.Title + "\" and all " + strconv.Itoa(p.PostCount) + " of its posts", + "action": r.URL.Path, + "back": "/b/" + currentBlog(r).Subdomain + "/pages", + "isHome": p.IsHome, + }) +} + +func (s *Server) handlePageDelete(w http.ResponseWriter, r *http.Request) { + p := s.loadPage(w, r) + if p == nil { + return + } + if p.IsHome { + s.plainError(w, http.StatusBadRequest, "The home page cannot be deleted. Make another page the home page first.") + return + } + if err := s.st.DeletePage(r.Context(), p.BlogID, p.ID); err != nil { + s.serverError(w, err) + return + } + redirectOK(w, r, "/b/"+currentBlog(r).Subdomain+"/pages", "Page deleted.") +} + +func (s *Server) handlePageMove(w http.ResponseWriter, r *http.Request) { + p := s.loadPage(w, r) + if p == nil { + return + } + dir := 1 + if r.FormValue("dir") == "up" { + dir = -1 + } + if err := s.st.MovePage(r.Context(), p.BlogID, p.ID, dir); err != nil { + s.serverError(w, err) + return + } + http.Redirect(w, r, "/b/"+currentBlog(r).Subdomain+"/pages", http.StatusSeeOther) +} + +func (s *Server) handlePageHome(w http.ResponseWriter, r *http.Request) { + p := s.loadPage(w, r) + if p == nil { + return + } + if err := s.st.SetHomePage(r.Context(), p.BlogID, p.ID); err != nil { + s.serverError(w, err) + return + } + redirectOK(w, r, "/b/"+currentBlog(r).Subdomain+"/pages", "\""+p.Title+"\" is now the home page.") +} |
