aboutsummaryrefslogtreecommitdiffstats
path: root/internal/store/modules.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/store/modules.go')
-rw-r--r--internal/store/modules.go136
1 files changed, 136 insertions, 0 deletions
diff --git a/internal/store/modules.go b/internal/store/modules.go
new file mode 100644
index 0000000..c7d9760
--- /dev/null
+++ b/internal/store/modules.go
@@ -0,0 +1,136 @@
+package store
+
+import (
+ "context"
+ "time"
+)
+
+// Module is one building block of a blog's layout: a menu, an archive, a
+// block of custom HTML… placed in one of the areas of the page.
+type Module struct {
+ ID int64
+ BlogID int64
+ Area string // header | left | right | above | below | footer
+ Kind string // title | logo | menu | archive | recent | html | rss | text | sitemap
+ Title string // optional heading (side columns and footer)
+ Body string // custom HTML, or the footer text
+ Count int // recent posts to show
+ SortOrder int
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+const moduleCols = `id, blog_id, area, kind, title, body, count, sort_order, created_at, updated_at`
+
+func scanModule(row interface{ Scan(...any) error }) (*Module, error) {
+ var m Module
+ err := row.Scan(&m.ID, &m.BlogID, &m.Area, &m.Kind, &m.Title, &m.Body, &m.Count, &m.SortOrder, &m.CreatedAt, &m.UpdatedAt)
+ if err != nil {
+ return nil, wrap(err)
+ }
+ return &m, nil
+}
+
+// ListModules returns every module of a blog, grouped by area in display order.
+func (s *Store) ListModules(ctx context.Context, blogID int64) ([]Module, error) {
+ rows, err := s.db.Query(ctx, `SELECT `+moduleCols+` FROM modules WHERE blog_id=$1 ORDER BY area, sort_order, id`, blogID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var out []Module
+ for rows.Next() {
+ m, err := scanModule(rows)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, *m)
+ }
+ return out, rows.Err()
+}
+
+func (s *Store) ModuleByID(ctx context.Context, blogID, id int64) (*Module, error) {
+ return scanModule(s.db.QueryRow(ctx, `SELECT `+moduleCols+` FROM modules WHERE blog_id=$1 AND id=$2`, blogID, id))
+}
+
+// CreateModule appends a module to the end of its area.
+func (s *Store) CreateModule(ctx context.Context, m *Module) (*Module, error) {
+ return scanModule(s.db.QueryRow(ctx, `INSERT INTO modules (blog_id, area, kind, title, body, count, sort_order)
+ VALUES ($1,$2,$3,$4,$5,$6,(SELECT coalesce(max(sort_order),-1)+1 FROM modules WHERE blog_id=$1 AND area=$2)) RETURNING `+moduleCols,
+ m.BlogID, m.Area, m.Kind, m.Title, m.Body, m.Count))
+}
+
+// UpdateModule saves the editable settings; area and kind never change.
+func (s *Store) UpdateModule(ctx context.Context, m *Module) error {
+ _, err := s.db.Exec(ctx, `UPDATE modules SET title=$3, body=$4, count=$5, updated_at=now() WHERE blog_id=$1 AND id=$2`,
+ m.BlogID, m.ID, m.Title, m.Body, m.Count)
+ return wrap(err)
+}
+
+func (s *Store) DeleteModule(ctx context.Context, blogID, id int64) error {
+ _, err := s.db.Exec(ctx, `DELETE FROM modules WHERE blog_id=$1 AND id=$2`, blogID, id)
+ return err
+}
+
+// MoveModule swaps sort_order with the neighbouring module of the same area (dir = -1 up, +1 down).
+func (s *Store) MoveModule(ctx context.Context, blogID, id int64, dir int) error {
+ cur, err := s.ModuleByID(ctx, blogID, id)
+ if err != nil {
+ return err
+ }
+ all, err := s.ListModules(ctx, blogID)
+ 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 := s.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=$3 WHERE blog_id=$1 AND id=$2`, blogID, m.ID, i); err != nil {
+ return err
+ }
+ }
+ return tx.Commit(ctx)
+}
+
+// ResetModules puts the layout back to what a new blog starts with.
+func (s *Store) ResetModules(ctx context.Context, blogID int64) error {
+ tx, err := s.db.Begin(ctx)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback(ctx)
+ if _, err := tx.Exec(ctx, `DELETE FROM modules WHERE blog_id=$1`, blogID); err != nil {
+ return err
+ }
+ if err := insertDefaultModules(ctx, tx, blogID); err != nil {
+ return err
+ }
+ return tx.Commit(ctx)
+}
+
+// insertDefaultModules is the starting layout: title and menu in the header,
+// an RSS link in the footer. Must match what the layout migration gave old blogs.
+func insertDefaultModules(ctx context.Context, tx querier, blogID int64) error {
+ _, err := tx.Exec(ctx, `INSERT INTO modules (blog_id, area, kind, sort_order) VALUES
+ ($1,'header','title',0), ($1,'header','menu',1), ($1,'footer','rss',0)`, blogID)
+ return err
+}