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 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, 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.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 the blog, grouped by area in display order. func (bs *BlogStore) ListModules(ctx context.Context) ([]Module, error) { rows, err := bs.db.Query(ctx, `SELECT `+moduleCols+` FROM modules ORDER BY area, sort_order, id`) 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 (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) VALUES ($1,$2,$3,$4,$5,(SELECT coalesce(max(sort_order),-1)+1 FROM modules WHERE area=$1)) RETURNING `+moduleCols, 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) if err != nil { return err } defer tx.Rollback(ctx) if _, err := tx.Exec(ctx, `DELETE FROM modules`); err != nil { return err } if err := insertDefaultModules(ctx, tx); 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. func insertDefaultModules(ctx context.Context, tx querier) error { _, err := tx.Exec(ctx, `INSERT INTO modules (area, kind, sort_order) VALUES ('header','title',0), ('header','menu',1), ('footer','rss',0)`) return err }