diff options
Diffstat (limited to 'internal/web/handlers_design.go')
| -rw-r--r-- | internal/web/handlers_design.go | 188 |
1 files changed, 159 insertions, 29 deletions
diff --git a/internal/web/handlers_design.go b/internal/web/handlers_design.go index 3e9e600..5853436 100644 --- a/internal/web/handlers_design.go +++ b/internal/web/handlers_design.go @@ -4,63 +4,187 @@ import ( "net/http" "time" + "github.com/google/uuid" "github.com/gramanas/blogspace/internal/store" ) -func (s *Server) handleDesignForm(w http.ResponseWriter, r *http.Request) { +// The Design tab: one form for the whole look of the blog — colours, fonts, +// images, which areas are shown and how wide, the modules of each area and +// the menu — saved in one go. + +// recentImages is how many library images the pickers list up front; the +// rest are fetched by the script's library panel, page by page. +const recentImages = 48 + +// designView is the state the design page shows: what the database holds, or +// what a rejected form sent, so no edit is lost to a validation message. +type designView struct { + Theme Theme + Modules []store.Module + Menu []store.MenuItem +} + +// designSections are the cards of the page, in order; the form says which one +// was in view when it was sent so the flash can return there. +var designSections = []string{"colours", "header", "menu", "content", "columns", "footer", "logo"} + +// modRow and menuRow are what the template renders a module or menu entry +// from: the row's field index (unique across the form) and its position in +// its list, the value of the no-script position box. +type modRow struct { + I, Pos int + M store.Module +} + +type menuRow struct { + I, Pos int + Item store.MenuItem +} + +// modTemplate is a row the script clones when a module is added. +type modTemplate struct { + Area, Kind string + M store.Module +} + +func (s *Server) loadDesign(r *http.Request) (designView, error) { blog := currentBlog(r) - images, _, err := blogStore(r).ListFiles(r.Context(), "image", "", 0, 0) + mods, err := blogStore(r).ListModules(r.Context()) if err != nil { - s.serverError(w, err) - return + return designView{}, err + } + menu, err := blogStore(r).ListMenu(r.Context()) + if err != nil { + return designView{}, err + } + return designView{Theme: ParseTheme(blog.ThemeJSON), Modules: mods, Menu: menu}, nil +} + +// designData is everything design.html needs; "today" is the sample for the date formats. +func (s *Server) designData(r *http.Request, v designView) (map[string]any, error) { + bs := blogStore(r) + pages, err := bs.ListPages(r.Context()) + if err != nil { + return nil, err + } + images, imageCount, err := bs.ListFiles(r.Context(), "image", "", recentImages, 0) + if err != nil { + return nil, err + } + // The pickers list the recent images; a chosen one older than that still + // needs its name for the select. + names := map[string]string{} + for _, f := range images { + names[f.ID.String()] = f.Filename + } + for _, id := range []string{v.Theme.BgImage, v.Theme.HeaderImage, v.Theme.Logo, v.Theme.Favicon} { + if id == "" || names[id] != "" { + continue + } + if f, err := bs.FileMeta(r.Context(), uuid.MustParse(id)); err == nil { // a deleted file was cleared from the theme already + names[id] = f.Filename + } + } + rows := map[string][]modRow{} + for i, m := range v.Modules { + rows[m.Area] = append(rows[m.Area], modRow{I: i, Pos: len(rows[m.Area]), M: m}) + } + var menuRows []menuRow + inMenu := map[int64]bool{} + for i, it := range v.Menu { + menuRows = append(menuRows, menuRow{I: i, Pos: i, Item: it}) + if it.PageID != nil { + inMenu[*it.PageID] = true + } + } + var freePages []store.Page // pages not yet in the menu + for _, p := range pages { + if !inMenu[p.ID] { + freePages = append(freePages, p) + } } - s.render(w, r, "dashboard/design.html", designData(ParseTheme(blog.ThemeJSON), images)) + var templates []modTemplate + for _, area := range Areas { + for _, kind := range moduleKinds[area] { + templates = append(templates, modTemplate{Area: area, Kind: kind, M: newModule(area, kind, currentBlog(r).Language)}) + } + } + var pageID int64 + return map[string]any{ + "theme": v.Theme, "rows": rows, "menuRows": menuRows, "freePages": freePages, "nextIndex": max(len(v.Modules), len(v.Menu)), + "templates": templates, "pageItem": store.MenuItem{PageID: &pageID}, "linkItem": store.MenuItem{}, + "kinds": moduleKinds, "names": moduleNames, "areaNames": areaNames, "single": singleModuleAreas, + "counts": []int{3, 5, 8, 10, 15, 20}, "tagCounts": []int{5, 10, 20, 30, 50}, "perPage": postsPerPageChoices, + "images": images, "imageNames": names, "imageCount": imageCount, "presets": Presets(), "today": time.Now(), + }, nil } -// designData is what design.html shows; "today" is the sample for the date formats. -func designData(theme Theme, images []store.File) map[string]any { - return map[string]any{"theme": theme, "images": images, "presets": Presets(), "today": time.Now()} +func (s *Server) handleDesignForm(w http.ResponseWriter, r *http.Request) { + v, err := s.loadDesign(r) + if err != nil { + s.serverError(w, err) + return + } + d, err := s.designData(r, v) + if err != nil { + s.serverError(w, err) + return + } + s.render(w, r, "dashboard/design.html", d) } func (s *Server) handleDesign(w http.ResponseWriter, r *http.Request) { blog := currentBlog(r) - theme := ThemeFromForm(ParseTheme(blog.ThemeJSON), r.Form) - // Optional direct uploads from the design form. - for field, dst := range map[string]*string{"bg_image_file": &theme.BgImage, "header_image_file": &theme.HeaderImage, "logo_file": &theme.Logo, "favicon_file": &theme.Favicon} { + lang := s.lang(r) + v := designView{Theme: ThemeFromForm(ParseTheme(blog.ThemeJSON), r.Form)} + // Without the script a scheme is a select, applied on top of the colour inputs. + if p, ok := PresetByKey(r.FormValue("preset")); ok { + v.Theme = v.Theme.WithPreset(p) + } + // The form is shown again with everything that was sent, plus the message. + fail := func(msg string) { + d, err := s.designData(r, v) + if err != nil { + s.serverError(w, err) + return + } + d["error"] = msg + s.renderStatus(w, r, http.StatusBadRequest, "dashboard/design.html", d) + } + // Optional direct uploads from the pickers. + for field, dst := range map[string]*string{"bg_image_file": &v.Theme.BgImage, "header_image_file": &v.Theme.HeaderImage, "logo_file": &v.Theme.Logo, "favicon_file": &v.Theme.Favicon} { img, err := s.readUpload(r, field, true) if err != nil { - images, _, _ := blogStore(r).ListFiles(r.Context(), "image", "", 0, 0) - d := designData(theme, images) - d["error"] = err.Error() - s.renderStatus(w, r, http.StatusBadRequest, "dashboard/design.html", d) + fail(err.Error()) return } if img != nil { *dst = img.ID.String() } } - if err := blogStore(r).UpdateTheme(r.Context(), theme.JSON()); err != nil { + var err error + if v.Modules, err = parseModules(r.Form, lang, blog.Language); err != nil { + fail(err.Error()) + return + } + pages, err := blogStore(r).ListPages(r.Context()) + if err != nil { s.serverError(w, err) return } - redirectOK(w, r, "/b/"+blog.Subdomain+"/design", s.tr(r, "Design saved. Refresh your blog to see it.")) -} - -// handleDesignPreset applies a colour scheme straight away; it is a separate -// form so it cannot be confused with the unsaved edits of the main one. -func (s *Server) handleDesignPreset(w http.ResponseWriter, r *http.Request) { - blog := currentBlog(r) - p, ok := PresetByKey(r.FormValue("preset")) - if !ok { - s.plainError(w, http.StatusBadRequest, s.tr(r, "Unknown colour scheme.")) + if v.Menu, err = parseMenu(r.Form, lang, pages); err != nil { + fail(err.Error()) return } - theme := ParseTheme(blog.ThemeJSON).WithPreset(p) - if err := blogStore(r).UpdateTheme(r.Context(), theme.JSON()); err != nil { + if err := blogStore(r).SaveDesign(r.Context(), v.Theme.JSON(), v.Modules, v.Menu); err != nil { s.serverError(w, err) return } - redirectOK(w, r, "/b/"+blog.Subdomain+"/design", s.trf(r, "Applied the %s scheme. Refresh your blog to see it.", s.tr(r, p.Name))) + back := "/b/" + blog.Subdomain + "/design" + if at := r.FormValue("at"); at != "" && pick(at, designSections...) == at { + back += "#" + at + } + redirectOK(w, r, back, s.tr(r, "Design saved. Refresh your blog to see it.")) } // handleDesignReset restores the defaults, layout modules included; images @@ -77,3 +201,9 @@ func (s *Server) handleDesignReset(w http.ResponseWriter, r *http.Request) { } redirectOK(w, r, "/b/"+blog.Subdomain+"/design", s.tr(r, "Design and layout reset to the defaults.")) } + +// handleLayoutRedirect keeps old links to the Layout tab working: it is the +// columns section of the Design tab now. +func (s *Server) handleLayoutRedirect(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/b/"+currentBlog(r).Subdomain+"/design#columns", http.StatusSeeOther) +} |
