diff options
| author | grm <grm@eyesin.space> | 2026-09-16 19:10:13 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-16 19:10:13 +0300 |
| commit | 4a6f987bf7f9d73e9dd33d225fd2d73e9bf69a88 (patch) | |
| tree | cc39ea660046d9c977647e7c9c36349c126be214 /internal/store | |
| parent | 6b6b5d2e35ff182a0732da245f4eb37c8afa0564 (diff) | |
| download | blogspace-4a6f987bf7f9d73e9dd33d225fd2d73e9bf69a88.tar.gz blogspace-4a6f987bf7f9d73e9dd33d225fd2d73e9bf69a88.tar.bz2 blogspace-4a6f987bf7f9d73e9dd33d225fd2d73e9bf69a88.zip | |
Merge the Layout and Design tabs into one form with a single Save
The look and the layout of a blog were split over two tabs that cross-linked
each other, and the Layout tab saved every toggle, move and add straight
away through a dozen tiny forms. Now one Design tab holds it all, organised
by part of the blog (colours & fonts, header, menu, content, side columns,
footer, logo & icon) behind a sticky side index, and one Save stores the
theme, every module and the menu atomically (BlogStore.SaveDesign). Module
settings are edited inline; Discard changes throws the edits away; a
validation error shows the form again exactly as it was sent.
The image pickers no longer render the whole library four times over: a
select of the newest images, and a script-driven panel that fetches the
library page by page (GET /files as JSON) with lazy thumbnails.
New options: posts per page, site title size, footer alignment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/store')
| -rw-r--r-- | internal/store/design.go | 69 | ||||
| -rw-r--r-- | internal/store/menu.go | 42 | ||||
| -rw-r--r-- | internal/store/modules.go | 55 |
3 files changed, 69 insertions, 97 deletions
diff --git a/internal/store/design.go b/internal/store/design.go new file mode 100644 index 0000000..4556d96 --- /dev/null +++ b/internal/store/design.go @@ -0,0 +1,69 @@ +package store + +import ( + "context" + "encoding/json" +) + +// SaveDesign stores the design form in one go: the theme, the modules of every +// area and the menu, so a half-applied save is impossible. mods and menu are +// the complete new state in display order: rows with an ID are updated, rows +// without one inserted, and anything else is deleted. +func (bs *BlogStore) SaveDesign(ctx context.Context, theme json.RawMessage, mods []Module, menu []MenuItem) error { + tx, err := bs.db.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + if _, err := tx.Exec(ctx, `UPDATE settings SET theme=$1, updated_at=now()`, theme); err != nil { + return err + } + + keep := []int64{} + for _, m := range mods { + if m.ID != 0 { + keep = append(keep, m.ID) + } + } + if _, err := tx.Exec(ctx, `DELETE FROM modules WHERE id <> ALL($1)`, keep); err != nil { + return err + } + order := map[string]int{} // sort_order restarts in every area + for _, m := range mods { + pos := order[m.Area] + order[m.Area]++ + if m.ID != 0 { // area and kind never change + _, err = tx.Exec(ctx, `UPDATE modules SET title=$2, body=$3, count=$4, sort_order=$5, updated_at=now() WHERE id=$1`, m.ID, m.Title, m.Body, m.Count, pos) + } else { + _, err = tx.Exec(ctx, `INSERT INTO modules (area, kind, title, body, count, sort_order) VALUES ($1,$2,$3,$4,$5,$6)`, m.Area, m.Kind, m.Title, m.Body, m.Count, pos) + } + if err != nil { + return wrap(err) + } + } + + keep = keep[:0] + for _, it := range menu { + if it.ID != 0 { + keep = append(keep, it.ID) + } + } + if _, err := tx.Exec(ctx, `DELETE FROM menu_items WHERE id <> ALL($1)`, keep); err != nil { + return err + } + for i, it := range menu { + switch { + case it.ID != 0: + _, err = tx.Exec(ctx, `UPDATE menu_items SET label=$2, url=$3, sort_order=$4 WHERE id=$1`, it.ID, it.Label, it.URL, i) + case it.PageID != nil: // a page deleted meanwhile simply adds nothing + _, err = tx.Exec(ctx, `INSERT INTO menu_items (page_id, sort_order) SELECT id, $2 FROM pages WHERE id=$1 + ON CONFLICT (page_id) WHERE page_id IS NOT NULL DO UPDATE SET sort_order=excluded.sort_order`, *it.PageID, i) + default: + _, err = tx.Exec(ctx, `INSERT INTO menu_items (label, url, sort_order) VALUES ($1,$2,$3)`, it.Label, it.URL, i) + } + if err != nil { + return wrap(err) + } + } + return tx.Commit(ctx) +} diff --git a/internal/store/menu.go b/internal/store/menu.go index cedb7ad..e2314af 100644 --- a/internal/store/menu.go +++ b/internal/store/menu.go @@ -76,45 +76,3 @@ func (bs *BlogStore) SetPageInMenu(ctx context.Context, pageID int64, on bool) e _, err := bs.db.Exec(ctx, `DELETE FROM menu_items WHERE page_id=$1`, pageID) return err } - -// AddMenuLink appends a custom link to the menu. -func (bs *BlogStore) AddMenuLink(ctx context.Context, label, url string) error { - _, err := bs.db.Exec(ctx, `INSERT INTO menu_items (label, url, sort_order) - VALUES ($1,$2,(SELECT coalesce(max(sort_order),-1)+1 FROM menu_items))`, label, url) - return err -} - -func (bs *BlogStore) DeleteMenuItem(ctx context.Context, id int64) error { - _, err := bs.db.Exec(ctx, `DELETE FROM menu_items WHERE id=$1`, id) - return err -} - -// MoveMenuItem swaps sort_order with the neighbouring entry (dir = -1 up, +1 down). -func (bs *BlogStore) MoveMenuItem(ctx context.Context, id int64, dir int) error { - items, err := bs.ListMenu(ctx) - if err != nil { - return err - } - idx := -1 - for i := range items { - if items[i].ID == id { - idx = i - } - } - j := idx + dir - if idx < 0 || j < 0 || j >= len(items) { - return nil - } - items[idx], items[j] = items[j], items[idx] - tx, err := bs.db.Begin(ctx) - if err != nil { - return err - } - defer tx.Rollback(ctx) - for i, m := range items { // renumber everything; keeps orders dense - if _, err := tx.Exec(ctx, `UPDATE menu_items SET sort_order=$2 WHERE id=$1`, m.ID, i); err != nil { - return err - } - } - return tx.Commit(ctx) -} diff --git a/internal/store/modules.go b/internal/store/modules.go index d077975..99aed1a 100644 --- a/internal/store/modules.go +++ b/internal/store/modules.go @@ -48,10 +48,6 @@ func (bs *BlogStore) ListModules(ctx context.Context) ([]Module, error) { return out, rows.Err() } -func (bs *BlogStore) ModuleByID(ctx context.Context, id int64) (*Module, error) { - return scanModule(bs.db.QueryRow(ctx, `SELECT `+moduleCols+` FROM modules WHERE id=$1`, id)) -} - // CreateModule appends a module to the end of its area. func (bs *BlogStore) CreateModule(ctx context.Context, m *Module) (*Module, error) { return scanModule(bs.db.QueryRow(ctx, `INSERT INTO modules (area, kind, title, body, count, sort_order) @@ -59,57 +55,6 @@ func (bs *BlogStore) CreateModule(ctx context.Context, m *Module) (*Module, erro m.Area, m.Kind, m.Title, m.Body, m.Count)) } -// UpdateModule saves the editable settings; area and kind never change. -func (bs *BlogStore) UpdateModule(ctx context.Context, m *Module) error { - _, err := bs.db.Exec(ctx, `UPDATE modules SET title=$2, body=$3, count=$4, updated_at=now() WHERE id=$1`, - m.ID, m.Title, m.Body, m.Count) - return wrap(err) -} - -func (bs *BlogStore) DeleteModule(ctx context.Context, id int64) error { - _, err := bs.db.Exec(ctx, `DELETE FROM modules WHERE id=$1`, id) - return err -} - -// MoveModule swaps sort_order with the neighbouring module of the same area (dir = -1 up, +1 down). -func (bs *BlogStore) MoveModule(ctx context.Context, id int64, dir int) error { - cur, err := bs.ModuleByID(ctx, id) - if err != nil { - return err - } - all, err := bs.ListModules(ctx) - if err != nil { - return err - } - var mods []Module - idx := -1 - for _, m := range all { - if m.Area != cur.Area { - continue - } - if m.ID == id { - idx = len(mods) - } - mods = append(mods, m) - } - j := idx + dir - if idx < 0 || j < 0 || j >= len(mods) { - return nil - } - mods[idx], mods[j] = mods[j], mods[idx] - tx, err := bs.db.Begin(ctx) - if err != nil { - return err - } - defer tx.Rollback(ctx) - for i, m := range mods { // renumber the area; keeps orders dense - if _, err := tx.Exec(ctx, `UPDATE modules SET sort_order=$2 WHERE id=$1`, m.ID, i); err != nil { - return err - } - } - return tx.Commit(ctx) -} - // ResetModules puts the layout back to what a new blog starts with. func (bs *BlogStore) ResetModules(ctx context.Context) error { tx, err := bs.db.Begin(ctx) |
