package store import ( "context" "time" ) // Section is an announcement shown on every page and post of a blog. type Section struct { ID int64 BlogID int64 Title string BodyMD string BodyHTML string Placement string // above | below | sidebar Style string // plain | note | warning Enabled bool SortOrder int CreatedAt time.Time UpdatedAt time.Time } const sectionCols = `id, blog_id, title, body_md, body_html, placement, style, enabled, sort_order, created_at, updated_at` func scanSection(row interface{ Scan(...any) error }) (*Section, error) { var s Section err := row.Scan(&s.ID, &s.BlogID, &s.Title, &s.BodyMD, &s.BodyHTML, &s.Placement, &s.Style, &s.Enabled, &s.SortOrder, &s.CreatedAt, &s.UpdatedAt) if err != nil { return nil, wrap(err) } return &s, nil } func (s *Store) collectSections(ctx context.Context, q string, args ...any) ([]Section, error) { rows, err := s.db.Query(ctx, q, args...) if err != nil { return nil, err } defer rows.Close() var out []Section for rows.Next() { sec, err := scanSection(rows) if err != nil { return nil, err } out = append(out, *sec) } return out, rows.Err() } func (s *Store) ListSections(ctx context.Context, blogID int64) ([]Section, error) { return s.collectSections(ctx, `SELECT `+sectionCols+` FROM sections WHERE blog_id=$1 ORDER BY sort_order, id`, blogID) } // EnabledSections is what the public blog renders. func (s *Store) EnabledSections(ctx context.Context, blogID int64) ([]Section, error) { return s.collectSections(ctx, `SELECT `+sectionCols+` FROM sections WHERE blog_id=$1 AND enabled ORDER BY sort_order, id`, blogID) } func (s *Store) SectionByID(ctx context.Context, blogID, id int64) (*Section, error) { return scanSection(s.db.QueryRow(ctx, `SELECT `+sectionCols+` FROM sections WHERE blog_id=$1 AND id=$2`, blogID, id)) } func (s *Store) CreateSection(ctx context.Context, sec *Section) (*Section, error) { return scanSection(s.db.QueryRow(ctx, `INSERT INTO sections (blog_id, title, body_md, body_html, placement, style, enabled, sort_order) VALUES ($1,$2,$3,$4,$5,$6,$7,(SELECT coalesce(max(sort_order),-1)+1 FROM sections WHERE blog_id=$1)) RETURNING `+sectionCols, sec.BlogID, sec.Title, sec.BodyMD, sec.BodyHTML, sec.Placement, sec.Style, sec.Enabled)) } func (s *Store) UpdateSection(ctx context.Context, sec *Section) error { _, err := s.db.Exec(ctx, `UPDATE sections SET title=$3, body_md=$4, body_html=$5, placement=$6, style=$7, enabled=$8, updated_at=now() WHERE blog_id=$1 AND id=$2`, sec.BlogID, sec.ID, sec.Title, sec.BodyMD, sec.BodyHTML, sec.Placement, sec.Style, sec.Enabled) return wrap(err) } func (s *Store) SetSectionEnabled(ctx context.Context, blogID, id int64, on bool) error { _, err := s.db.Exec(ctx, `UPDATE sections SET enabled=$3, updated_at=now() WHERE blog_id=$1 AND id=$2`, blogID, id, on) return err } func (s *Store) DeleteSection(ctx context.Context, blogID, id int64) error { _, err := s.db.Exec(ctx, `DELETE FROM sections WHERE blog_id=$1 AND id=$2`, blogID, id) return err } // MoveSection swaps sort_order with the neighbouring section (dir = -1 up, +1 down). func (s *Store) MoveSection(ctx context.Context, blogID, id int64, dir int) error { secs, err := s.ListSections(ctx, blogID) if err != nil { return err } idx := -1 for i := range secs { if secs[i].ID == id { idx = i } } j := idx + dir if idx < 0 || j < 0 || j >= len(secs) { return nil } secs[idx], secs[j] = secs[j], secs[idx] tx, err := s.db.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) for i, sec := range secs { // renumber everything; keeps orders dense if _, err := tx.Exec(ctx, `UPDATE sections SET sort_order=$3 WHERE blog_id=$1 AND id=$2`, blogID, sec.ID, i); err != nil { return err } } return tx.Commit(ctx) }