From 7ac90f8e99dbf763b0fe04a17d7eef19dd22cae8 Mon Sep 17 00:00:00 2001 From: grm Date: Fri, 18 Sep 2026 14:35:59 +0300 Subject: Revert "Special pages: a gallery page of the blog's images" This reverts commit 0fc470a. The feature was not wanted. Migration 00013 stays on disk because databases have already run it (goose refuses a missing applied migration); 00014 drops the column and table it added. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/web/handlers_blog.go | 19 +---- internal/web/handlers_pages.go | 107 ++---------------------- internal/web/handlers_posts.go | 6 +- internal/web/static/blog.css | 5 -- internal/web/static/dashboard.css | 17 ++-- internal/web/templates/blog/gallery.html | 9 -- internal/web/templates/dashboard/overview.html | 2 +- internal/web/templates/dashboard/page_form.html | 15 +--- internal/web/templates/dashboard/pages.html | 6 +- internal/web/templates/dashboard/post_form.html | 2 +- internal/web/templates/dashboard/posts.html | 2 +- internal/web/web_test.go | 4 +- 12 files changed, 29 insertions(+), 165 deletions(-) delete mode 100644 internal/web/templates/blog/gallery.html (limited to 'internal/web') diff --git a/internal/web/handlers_blog.go b/internal/web/handlers_blog.go index 006129c..55cec2e 100644 --- a/internal/web/handlers_blog.go +++ b/internal/web/handlers_blog.go @@ -107,20 +107,6 @@ func (s *Server) renderPage(w http.ResponseWriter, r *http.Request, page *store. s.serverError(w, err) return } - v["page"] = page - if !page.ShowNotices { - v["notices"] = noNotices - } - if page.Kind == store.PageKindGallery { // a special page: its images instead of posts - images, err := blogStore(r).GalleryImages(r.Context(), page.ID, themeFiles(v["theme"].(Theme))) - if err != nil { - s.serverError(w, err) - return - } - v["images"] = images - s.render(w, r, "blog/gallery.html", v) - return - } n, per := pageNum(r), v["theme"].(Theme).PostsPerPage posts, total, err := blogStore(r).PublishedPosts(r.Context(), page.ID, per, (n-1)*per) if err != nil { @@ -129,7 +115,10 @@ func (s *Server) renderPage(w http.ResponseWriter, r *http.Request, page *store. } last := (total + per - 1) / per fillExcerpts(v["theme"].(Theme), posts) - v["posts"], v["pageNum"], v["lastPage"] = posts, n, last + v["page"], v["posts"], v["pageNum"], v["lastPage"] = page, posts, n, last + if !page.ShowNotices { + v["notices"] = noNotices + } // The pager appends ?p=N to base: no trailing slash, which /{page} would not match. v["base"] = "/" + page.Slug if page.IsHome { diff --git a/internal/web/handlers_pages.go b/internal/web/handlers_pages.go index 0a2c9c4..b085b82 100644 --- a/internal/web/handlers_pages.go +++ b/internal/web/handlers_pages.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" - "github.com/google/uuid" "github.com/gramanas/blogspace/internal/slug" "github.com/gramanas/blogspace/internal/store" ) @@ -39,86 +38,19 @@ func (s *Server) loadPage(w http.ResponseWriter, r *http.Request) *store.Page { return p } -// pageKind reads a new page's kind from the form or query (?kind= on the -// "New special page" link, a hidden field on the POST). An existing page -// keeps its own. -func pageKind(r *http.Request) string { - return pick(r.FormValue("kind"), store.PageKindPosts, store.PageKindGallery) -} - -// themeFiles are the images the design uses (logo, favicon, header and -// background image): decoration, not content, so a gallery never shows them. -func themeFiles(t Theme) []uuid.UUID { - var ids []uuid.UUID - for _, v := range []string{t.Logo, t.Favicon, t.HeaderImage, t.BgImage} { - if id, err := uuid.Parse(v); err == nil { - ids = append(ids, id) - } - } - return ids -} - -// pageFormData is what the page form needs besides the page. For a gallery -// that is every library image but the theme's, and which ones the page -// hides — an unsaved page hides none. -func (s *Server) pageFormData(r *http.Request, p *store.Page) (map[string]any, error) { - d := map[string]any{"page": p} - if p.Kind != store.PageKindGallery { - return d, nil - } - images, _, err := blogStore(r).ListFiles(r.Context(), "image", "", 0, 0) - if err != nil { - return nil, err - } - skip := map[uuid.UUID]bool{} - for _, id := range themeFiles(ParseTheme(currentBlog(r).ThemeJSON)) { - skip[id] = true - } - shown := images[:0] - for _, f := range images { - if !skip[f.ID] { - shown = append(shown, f) - } - } - hidden := map[string]bool{} - if p.ID != 0 { - if hidden, err = blogStore(r).HiddenFiles(r.Context(), p.ID); err != nil { - return nil, err - } - } - d["images"], d["hidden"] = shown, hidden - return d, nil -} - -// hiddenFromForm is the "hide" checkboxes of a gallery form as ids; junk is dropped. -func hiddenFromForm(r *http.Request) []uuid.UUID { - ids := []uuid.UUID{} - for _, v := range r.Form["hide"] { - if id, err := uuid.Parse(v); err == nil { - ids = append(ids, id) - } - } - return ids -} - func (s *Server) handlePageForm(w http.ResponseWriter, r *http.Request) { - p := &store.Page{Kind: pageKind(r), ShowInNav: true, ShowNotices: true} + p := &store.Page{ShowInNav: true, ShowNotices: true} if r.PathValue("id") != "" { if p = s.loadPage(w, r); p == nil { return } } - d, err := s.pageFormData(r, p) - if err != nil { - s.serverError(w, err) - return - } - s.render(w, r, "dashboard/page_form.html", d) + 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{Kind: pageKind(r)} + p := &store.Page{} if r.PathValue("id") != "" { if p = s.loadPage(w, r); p == nil { return @@ -149,25 +81,8 @@ func (s *Server) handlePageSave(w http.ResponseWriter, r *http.Request) { case !slug.Valid(p.Slug) || reservedPageSlugs[p.Slug]: msg = s.tr(r, "Slug may only contain lowercase letters, digits and dashes (and not be a reserved word).") } - // The rejected form is rendered with what was sent, ticks included. - fail := func(status int, msg string) { - d, err := s.pageFormData(r, p) - if err != nil { - s.serverError(w, err) - return - } - if p.Kind == store.PageKindGallery { - hidden := map[string]bool{} - for _, id := range hiddenFromForm(r) { - hidden[id.String()] = true - } - d["hidden"] = hidden - } - d["error"] = msg - s.renderStatus(w, r, status, "dashboard/page_form.html", d) - } if msg != "" { - fail(http.StatusBadRequest, msg) + s.renderStatus(w, r, http.StatusBadRequest, "dashboard/page_form.html", map[string]any{"page": p, "error": msg}) return } p.IntroMD = appendFile(p.IntroMD, img, p.Format) @@ -191,18 +106,12 @@ func (s *Server) handlePageSave(w http.ResponseWriter, r *http.Request) { } if err != nil { if errors.Is(err, store.ErrConflict) { - fail(http.StatusConflict, s.tr(r, "A page with that slug already exists.")) + s.renderStatus(w, r, http.StatusConflict, "dashboard/page_form.html", map[string]any{"page": p, "error": s.tr(r, "A page with that slug already exists.")}) return } s.serverError(w, err) return } - if p.Kind == store.PageKindGallery { - if err := blogStore(r).SetHiddenFiles(r.Context(), p.ID, hiddenFromForm(r)); err != nil { - s.serverError(w, err) - return - } - } redirectOK(w, r, "/b/"+blog.Subdomain+"/pages/"+strconv.FormatInt(p.ID, 10)+"/edit", s.tr(r, "Saved. Refresh your blog to see it.")) } @@ -211,12 +120,8 @@ func (s *Server) handlePageDeleteConfirm(w http.ResponseWriter, r *http.Request) if p == nil { return } - what := s.trf(r, "the page “%s” and all %d of its posts", p.Title, p.PostCount) - if p.Special() { - what = s.trf(r, "the gallery page “%s”", p.Title) - } s.render(w, r, "dashboard/confirm.html", map[string]any{ - "what": what, + "what": s.trf(r, "the page “%s” and all %d of its posts", p.Title, p.PostCount), "action": r.URL.Path, "back": "/b/" + currentBlog(r).Subdomain + "/pages", "isHome": p.IsHome, diff --git a/internal/web/handlers_posts.go b/internal/web/handlers_posts.go index 0c34198..8a54c31 100644 --- a/internal/web/handlers_posts.go +++ b/internal/web/handlers_posts.go @@ -89,8 +89,8 @@ func (s *Server) handlePostForm(w http.ResponseWriter, r *http.Request) { } } else if pid, _ := strconv.ParseInt(r.URL.Query().Get("page"), 10, 64); pid != 0 { p.PageID = pid - } else if hp, err := blogStore(r).HomePage(r.Context()); err == nil && !hp.Special() { - p.PageID = hp.ID // a gallery home page takes no posts: the select's first page stands in + } else if hp, err := blogStore(r).HomePage(r.Context()); err == nil { + p.PageID = hp.ID } d, err := s.postFormData(r, p) if err != nil { @@ -153,7 +153,7 @@ func (s *Server) handlePostSave(w http.ResponseWriter, r *http.Request) { } pageOK := false for _, pg := range pages { - if pg.ID == p.PageID && !pg.Special() { + if pg.ID == p.PageID { pageOK = true } } diff --git a/internal/web/static/blog.css b/internal/web/static/blog.css index aa8357e..8cd969e 100644 --- a/internal/web/static/blog.css +++ b/internal/web/static/blog.css @@ -56,10 +56,6 @@ img { max-width: 100%; height: auto; } .post.has-thumb { overflow: hidden; } .post-image { margin: 0 0 1em; } .post-image img { display: block; max-width: 100%; margin: 0 auto; } -/* gallery page: a masonry wall from CSS columns alone; without them the pictures simply stack */ -.gallery { -webkit-column-width: 220px; -moz-column-width: 220px; column-width: 220px; -webkit-column-gap: 1em; -moz-column-gap: 1em; column-gap: 1em; } -.gallery a { display: block; margin: 0 0 1em; -webkit-column-break-inside: avoid; page-break-inside: avoid; break-inside: avoid; } -.gallery img { display: block; width: 100%; } .read-more { margin: 0.3em 0 0; font-weight: bold; } .read-more a { text-decoration: none; } .read-more a:hover { text-decoration: underline; } @@ -162,7 +158,6 @@ img { max-width: 100%; height: auto; } @media (max-width: 700px) { .content { padding: 1em; } .post-thumb { width: 40%; margin-left: 1em; } - .gallery { -webkit-column-width: 140px; -moz-column-width: 140px; column-width: 140px; } .site-title { font-size: 1.5em; } .ruled > .header-rule { display: none; } .ruled > .brand { white-space: normal; } .cols { display: block; } diff --git a/internal/web/static/dashboard.css b/internal/web/static/dashboard.css index 70df6fa..4aa0213 100644 --- a/internal/web/static/dashboard.css +++ b/internal/web/static/dashboard.css @@ -278,16 +278,11 @@ textarea.mono { font-size: 0.9em; } .picker-box { max-width: 860px; margin: 0 auto; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 8px 30px rgba(16, 24, 40, 0.25); padding: 1em 1.2em; } .picker-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6em; margin-bottom: 0.8em; } .picker-head input { flex: 1 1 12em; width: auto; margin: 0; padding: 0.4em 0.6em; } -/* .imagegrid is the wall of thumbnails: the picker's choices and the gallery page's Hide grid */ -.imagegrid, .picker .picks { display: flex; flex-wrap: wrap; gap: 0.7em; } -.imagegrid .pick, .picker .pick { display: block; width: 120px; margin: 0; padding: 0.4em; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); text-align: center; font-size: 0.75em; font-weight: normal; cursor: pointer; } -.imagegrid .pick:hover, .picker .pick:hover { border-color: var(--accent); background: var(--surface-2); color: var(--text); } -.imagegrid .pick img, .picker .pick img { display: block; width: 100%; height: 64px; margin: 0 auto 0.3em; border: 1px solid var(--border); border-radius: 4px; background: var(--surface-2); object-fit: contain; } -.imagegrid .pick .name, .picker .pick .name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.hidegrid { margin: 0 0 1em; } -.hidegrid .hide { display: block; margin-top: 0.3em; } -.hidegrid .hide input { width: auto; margin: 0 0.2em 0 0; vertical-align: middle; } -.hidegrid .pick:has(:checked) img { opacity: 0.35; } /* a ticked card dims; the tick itself is the signal elsewhere */ +.picker .picks { display: flex; flex-wrap: wrap; gap: 0.7em; } +.picker .pick { width: 120px; margin: 0; padding: 0.4em; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); text-align: center; font-size: 0.75em; font-weight: normal; cursor: pointer; } +.picker .pick:hover { border-color: var(--accent); background: var(--surface-2); color: var(--text); } +.picker .pick img { display: block; width: 100%; height: 64px; margin: 0 auto 0.3em; border: 1px solid var(--border); border-radius: 4px; background: var(--surface-2); object-fit: contain; } +.picker .pick .name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .picker .pager { margin: 0.8em 0 0; } .picker .pager button[disabled] { opacity: 0.4; cursor: default; } .savebar { position: sticky; bottom: 0; z-index: 5; margin: 0; padding: 0.8em 0; background: var(--bg); border-top: 1px solid var(--border-strong); display: flex; flex-wrap: wrap; gap: 0.8em; align-items: center; } @@ -308,7 +303,7 @@ details.reset { margin-top: 1.6em; } .cols { grid-template-columns: 1fr; gap: 1.2em; } .row { gap: 0; } .row label { flex-basis: 100%; } - .imagegrid .pick, .picker .pick { width: 46%; } + .picker .pick { width: 46%; } .presets .preset { width: 46%; } .sidenav { flex-wrap: nowrap; overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; margin: 0 -1em 1em; padding: 0 1em; } .pickrow { flex-wrap: wrap; } diff --git a/internal/web/templates/blog/gallery.html b/internal/web/templates/blog/gallery.html deleted file mode 100644 index 4dbde13..0000000 --- a/internal/web/templates/blog/gallery.html +++ /dev/null @@ -1,9 +0,0 @@ -{{define "title"}}{{if not .Data.page.IsHome}}{{.Data.page.Title}} · {{end}}{{.Blog.Title}}{{end}} -{{define "content"}} -{{if not .Data.page.IsHome}}

{{.Data.page.Title}}

{{end}} -{{if .Data.page.IntroHTML}}
{{html .Data.page.IntroHTML}}
{{end}} -{{if .Data.images}}{{else if not (or .Data.page.IntroHTML .Data.page.OutroHTML)}}

{{t "Nothing here yet."}}

{{end}} -{{if .Data.page.OutroHTML}}
{{html .Data.page.OutroHTML}}
{{end}} -{{end}} diff --git a/internal/web/templates/dashboard/overview.html b/internal/web/templates/dashboard/overview.html index 2a797d8..e239445 100644 --- a/internal/web/templates/dashboard/overview.html +++ b/internal/web/templates/dashboard/overview.html @@ -12,7 +12,7 @@

{{t "Pages"}}

    - {{range .Data.pages}}
  • {{.Title}} ({{if .Special}}{{t .Kind}}{{else}}{{tf "%d posts" .PostCount}}{{end}}{{if .IsHome}}, {{t "home"}}{{end}}{{if not .ShowInNav}}, {{t "hidden from menu"}}{{end}})
  • {{end}} + {{range .Data.pages}}
  • {{.Title}} ({{tf "%d posts" .PostCount}}{{if .IsHome}}, {{t "home"}}{{end}}{{if not .ShowInNav}}, {{t "hidden from menu"}}{{end}})
  • {{end}}
diff --git a/internal/web/templates/dashboard/page_form.html b/internal/web/templates/dashboard/page_form.html index 5b47d05..4ee720b 100644 --- a/internal/web/templates/dashboard/page_form.html +++ b/internal/web/templates/dashboard/page_form.html @@ -1,12 +1,9 @@ -{{define "title"}}{{template "pageformtitle" .}} · {{.Blog.Title}}{{end}} -{{define "pageformtitle"}}{{if eq .Data.page.Kind "gallery"}}{{if .Data.page.ID}}{{t "Edit gallery page"}}{{else}}{{t "New gallery page"}}{{end}}{{else}}{{if .Data.page.ID}}{{t "Edit page"}}{{else}}{{t "New page"}}{{end}}{{end}}{{end}} +{{define "title"}}{{if .Data.page.ID}}{{t "Edit page"}}{{else}}{{t "New page"}}{{end}} · {{.Blog.Title}}{{end}} {{define "content"}}
-

{{template "pageformtitle" .}}

- {{if eq .Data.page.Kind "gallery"}}

{{t "A gallery page shows every image in your files as a wall of pictures, newest first, between its intro and closing text."}}

{{end}} +

{{if .Data.page.ID}}{{t "Edit page"}}{{else}}{{t "New page"}}{{end}}

- {{if not .Data.page.ID}}{{end}} @@ -14,14 +11,6 @@ {{template "editor" (dict "name" "intro" "value" .Data.page.IntroMD "format" .Data.page.Format "rows" 8 "tall" false "upload" (print "/b/" .Blog.Subdomain "/files/upload") "preview" (print "/b/" .Blog.Subdomain "/preview") "csrf" .CSRF)}} {{template "editor" (dict "name" "outro" "value" .Data.page.OutroMD "format" .Data.page.Format "formatof" "intro" "rows" 8 "tall" false "upload" (print "/b/" .Blog.Subdomain "/files/upload") "preview" (print "/b/" .Blog.Subdomain "/preview") "csrf" .CSRF)}} - {{if eq .Data.page.Kind "gallery"}} -

{{t "Images"}}

-

{{t "Every image in your files is shown except the ones ticked here. The logo, favicon, header and background images are never shown."}}

- {{if .Data.images}}
- {{range .Data.images}}{{end}} -
- {{else}}

{{t "No images in your files yet."}} {{t "Files"}}

{{end}} - {{end}}

diff --git a/internal/web/templates/dashboard/pages.html b/internal/web/templates/dashboard/pages.html index af3bb5e..6c3b56d 100644 --- a/internal/web/templates/dashboard/pages.html +++ b/internal/web/templates/dashboard/pages.html @@ -2,7 +2,7 @@ {{define "content"}}

{{t "Pages"}}

- +

{{t "Each page holds its own list of posts. Which pages are in the menu, and in what order, is set on the"}} {{t "Design"}} {{t "tab."}}

@@ -14,9 +14,9 @@ {{.Title}} - {{if .Special}}{{t .Kind}}{{end}}{{if .IsHome}}{{t "home"}}{{end}}{{if not .ShowInNav}}{{t "not in menu"}}{{end}} + {{if .IsHome}}{{t "home"}}{{end}}{{if not .ShowInNav}}{{t "not in menu"}}{{end}} {{if .IsHome}}/{{else}}/{{.Slug}}{{end}} ↗ - {{if .Special}}—{{else}}{{.PostCount}}{{end}} + {{.PostCount}} {{if not .IsHome}}
{{t "delete"}}{{end}} diff --git a/internal/web/templates/dashboard/post_form.html b/internal/web/templates/dashboard/post_form.html index edb60fd..e64a331 100644 --- a/internal/web/templates/dashboard/post_form.html +++ b/internal/web/templates/dashboard/post_form.html @@ -7,7 +7,7 @@
diff --git a/internal/web/templates/dashboard/posts.html b/internal/web/templates/dashboard/posts.html index 217c966..983fbae 100644 --- a/internal/web/templates/dashboard/posts.html +++ b/internal/web/templates/dashboard/posts.html @@ -8,7 +8,7 @@ diff --git a/internal/web/web_test.go b/internal/web/web_test.go index 2d4df5c..2f3bc4f 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -643,7 +643,7 @@ func TestAllTemplatesParse(t *testing.T) { for _, name := range []string{"auth/login.html", "dashboard/overview.html", "dashboard/pages.html", "dashboard/page_form.html", "dashboard/posts.html", "dashboard/post_form.html", "dashboard/design.html", "dashboard/files.html", "dashboard/settings.html", "dashboard/password.html", "dashboard/confirm.html", "dashboard/sections.html", "dashboard/section_form.html", "admin/index.html", "admin/new_user.html", "admin/delete_user.html", - "blog/page.html", "blog/gallery.html", "blog/post.html", "blog/tag.html", "blog/search.html", "blog/404.html"} { + "blog/page.html", "blog/post.html", "blog/tag.html", "blog/search.html", "blog/404.html"} { for _, l := range i18n.Languages() { if _, err := tpl.get(l.Code, name); err != nil { t.Errorf("%s (%s): %v", name, l.Code, err) @@ -694,7 +694,7 @@ func TestGreekCatalogComplete(t *testing.T) { for _, p := range Presets() { used[p.Name] = true } - for _, k := range []string{"top", "bottom", store.RoleSuperadmin, store.RoleBlogger, store.PageKindGallery} { + for _, k := range []string{"top", "bottom", store.RoleSuperadmin, store.RoleBlogger} { used[k] = true } have := map[string]bool{} -- cgit v1.2.3