Have got post creation working.
This commit is contained in:
parent
63b19a249a
commit
8e0ffb6c24
|
@ -4,6 +4,66 @@
|
||||||
|
|
||||||
package dbq
|
package dbq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PostStateDraft PostState = "draft"
|
||||||
|
PostStatePublished PostState = "published"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *PostState) Scan(src interface{}) error {
|
||||||
|
switch s := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
*e = PostState(s)
|
||||||
|
case string:
|
||||||
|
*e = PostState(s)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported scan type for PostState: %T", src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullPostState struct {
|
||||||
|
PostState PostState
|
||||||
|
Valid bool // Valid is true if PostState is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (ns *NullPostState) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
ns.PostState, ns.Valid = "", false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ns.Valid = true
|
||||||
|
return ns.PostState.Scan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (ns NullPostState) Value() (driver.Value, error) {
|
||||||
|
if !ns.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return string(ns.PostState), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
Title pgtype.Text
|
||||||
|
Body string
|
||||||
|
State PostState
|
||||||
|
Props []byte
|
||||||
|
PostDate pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
type Site struct {
|
type Site struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
|
|
83
gen/sqlc/dbq/posts.sql.go
Normal file
83
gen/sqlc/dbq/posts.sql.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: posts.sql
|
||||||
|
|
||||||
|
package dbq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertPost = `-- name: InsertPost :one
|
||||||
|
INSERT INTO post (
|
||||||
|
site_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
state,
|
||||||
|
props,
|
||||||
|
post_date,
|
||||||
|
created_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertPostParams struct {
|
||||||
|
SiteID int64
|
||||||
|
Title pgtype.Text
|
||||||
|
Body string
|
||||||
|
State PostState
|
||||||
|
Props []byte
|
||||||
|
PostDate pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, insertPost,
|
||||||
|
arg.SiteID,
|
||||||
|
arg.Title,
|
||||||
|
arg.Body,
|
||||||
|
arg.State,
|
||||||
|
arg.Props,
|
||||||
|
arg.PostDate,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listPosts = `-- name: ListPosts :many
|
||||||
|
SELECT id, site_id, title, body, state, props, post_date, created_at FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listPosts, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Post
|
||||||
|
for rows.Next() {
|
||||||
|
var i Post
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Body,
|
||||||
|
&i.State,
|
||||||
|
&i.Props,
|
||||||
|
&i.PostDate,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -41,4 +41,6 @@ require (
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1 // indirect
|
||||||
)
|
)
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -81,3 +81,7 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1 h1:Seqp9vlIw3uJBL0V/eWIM3dAnSuToJ/cztkRQtl3g20=
|
||||||
|
lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||||
|
|
14
handlers/ctx.go
Normal file
14
handlers/ctx.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"lmika.dev/lmika/hugo-crm/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type siteKeyType struct{}
|
||||||
|
|
||||||
|
var siteKey siteKeyType
|
||||||
|
|
||||||
|
func GetSite(c *fiber.Ctx) models.Site {
|
||||||
|
return c.UserContext().Value(siteKey).(models.Site)
|
||||||
|
}
|
49
handlers/post.go
Normal file
49
handlers/post.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"lmika.dev/lmika/hugo-crm/services/posts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
Post *posts.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Post) Posts() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
site := GetSite(c)
|
||||||
|
|
||||||
|
posts, err := h.Post.ListPostOfSite(c.UserContext(), site)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render("sites/posts", fiber.Map{
|
||||||
|
"site": site,
|
||||||
|
"posts": posts,
|
||||||
|
}, "layouts/main")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Post) Create() fiber.Handler {
|
||||||
|
type Req struct {
|
||||||
|
Body string `json:"body" form:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
site := GetSite(c)
|
||||||
|
|
||||||
|
var req Req
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := h.Post.Create(c.UserContext(), site, req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"lmika.dev/lmika/hugo-crm/services/sites"
|
"lmika.dev/lmika/hugo-crm/services/sites"
|
||||||
|
@ -30,9 +31,29 @@ func (s *Site) Show() fiber.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
site, err := s.Site.GetSite(c.UserContext(), id)
|
site, err := s.Site.GetSite(c.UserContext(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.Render("sites/index", fiber.Map{
|
return c.Render("sites/index", fiber.Map{
|
||||||
"site": site,
|
"site": site,
|
||||||
}, "layouts/main")
|
}, "layouts/main")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Site) WithSite() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
id, err := c.ParamsInt("siteId")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
site, err := s.Site.GetSite(c.UserContext(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetUserContext(context.WithValue(c.UserContext(), siteKey, site))
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
9
main.go
9
main.go
|
@ -11,6 +11,7 @@ import (
|
||||||
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
||||||
"lmika.dev/lmika/hugo-crm/providers/themes"
|
"lmika.dev/lmika/hugo-crm/providers/themes"
|
||||||
"lmika.dev/lmika/hugo-crm/services/jobs"
|
"lmika.dev/lmika/hugo-crm/services/jobs"
|
||||||
|
"lmika.dev/lmika/hugo-crm/services/posts"
|
||||||
"lmika.dev/lmika/hugo-crm/services/sitebuilder"
|
"lmika.dev/lmika/hugo-crm/services/sitebuilder"
|
||||||
"lmika.dev/lmika/hugo-crm/services/sites"
|
"lmika.dev/lmika/hugo-crm/services/sites"
|
||||||
"lmika.dev/lmika/hugo-crm/templates"
|
"lmika.dev/lmika/hugo-crm/templates"
|
||||||
|
@ -41,8 +42,10 @@ func main() {
|
||||||
siteBuilderService := sitebuilder.New(themesProvider, gitProvider, hugoProvider)
|
siteBuilderService := sitebuilder.New(themesProvider, gitProvider, hugoProvider)
|
||||||
|
|
||||||
siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService)
|
siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService)
|
||||||
|
postService := posts.New(dbp, siteBuilderService, jobService)
|
||||||
|
|
||||||
siteHandlers := handlers.Site{Site: siteService}
|
siteHandlers := handlers.Site{Site: siteService}
|
||||||
|
postHandlers := handlers.Post{Post: postService}
|
||||||
|
|
||||||
log.Println("Connected to database")
|
log.Println("Connected to database")
|
||||||
if err := dbp.Migrate(context.Background()); err != nil {
|
if err := dbp.Migrate(context.Background()); err != nil {
|
||||||
|
@ -63,6 +66,12 @@ func main() {
|
||||||
app.Post("/sites", siteHandlers.Create())
|
app.Post("/sites", siteHandlers.Create())
|
||||||
app.Get("/sites/:siteId", siteHandlers.Show())
|
app.Get("/sites/:siteId", siteHandlers.Show())
|
||||||
|
|
||||||
|
siteGroup := app.Group("/sites/:siteId")
|
||||||
|
siteGroup.Use(siteHandlers.WithSite())
|
||||||
|
|
||||||
|
siteGroup.Get("/posts", postHandlers.Posts())
|
||||||
|
siteGroup.Post("/posts", postHandlers.Create())
|
||||||
|
|
||||||
jobService.Start()
|
jobService.Start()
|
||||||
defer jobService.Stop()
|
defer jobService.Stop()
|
||||||
|
|
||||||
|
|
20
models/posts.go
Normal file
20
models/posts.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type PostState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PostStateDraft PostState = "draft"
|
||||||
|
PostStatePublished PostState = "published"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
State PostState
|
||||||
|
PostDate time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
|
@ -3,4 +3,10 @@ package models
|
||||||
type ThemeMeta struct {
|
type ThemeMeta struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
URL string `json:"repo"`
|
URL string `json:"repo"`
|
||||||
|
|
||||||
|
// Indicates that this theme prefers posts have titles.
|
||||||
|
PreferTitle bool
|
||||||
|
|
||||||
|
// Content directory for "blog" posts
|
||||||
|
PostDir string `json:"post_dir"`
|
||||||
}
|
}
|
||||||
|
|
46
providers/db/posts.go
Normal file
46
providers/db/posts.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"lmika.dev/lmika/hugo-crm/gen/sqlc/dbq"
|
||||||
|
"lmika.dev/lmika/hugo-crm/models"
|
||||||
|
"lmika.dev/pkg/modash/moslice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *DB) ListPostsOfSite(ctx context.Context, siteID int64) ([]models.Post, error) {
|
||||||
|
res, err := db.q.ListPosts(ctx, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return moslice.Map(res, func(p dbq.Post) models.Post {
|
||||||
|
return models.Post{
|
||||||
|
ID: p.ID,
|
||||||
|
SiteID: p.SiteID,
|
||||||
|
Title: p.Title.String,
|
||||||
|
Body: p.Body,
|
||||||
|
State: models.PostState(p.State),
|
||||||
|
PostDate: p.PostDate.Time,
|
||||||
|
CreatedAt: p.CreatedAt.Time,
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
|
||||||
|
res, err := db.q.InsertPost(ctx, dbq.InsertPostParams{
|
||||||
|
SiteID: p.SiteID,
|
||||||
|
Title: pgtype.Text{String: p.Title, Valid: p.Title != ""},
|
||||||
|
Body: p.Body,
|
||||||
|
State: dbq.PostState(p.State),
|
||||||
|
Props: []byte(`{}`),
|
||||||
|
PostDate: pgtype.Timestamptz{Time: p.PostDate, Valid: !p.PostDate.IsZero()},
|
||||||
|
CreatedAt: pgtype.Timestamp{Time: p.CreatedAt, Valid: !p.CreatedAt.IsZero()},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ID = res
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package hugo
|
||||||
type SiteDir string
|
type SiteDir string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BaseSiteDir SiteDir = "base"
|
BaseSiteDir SiteDir = "base"
|
||||||
ThemeSiteDir SiteDir = "theme"
|
ThemeSiteDir SiteDir = "theme"
|
||||||
|
ContentSiteDir SiteDir = "content"
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (p *Provider) SiteStagingDir(site models.Site, what SiteDir) string {
|
||||||
case ThemeSiteDir:
|
case ThemeSiteDir:
|
||||||
return filepath.Join(p.stagingDir, site.Name, "themes", site.Theme)
|
return filepath.Join(p.stagingDir, site.Name, "themes", site.Theme)
|
||||||
}
|
}
|
||||||
return ""
|
return filepath.Join(p.stagingDir, site.Name, string(what))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) NewSite(ctx context.Context, site models.Site) error {
|
func (p *Provider) NewSite(ctx context.Context, site models.Site) error {
|
||||||
|
|
|
@ -4,7 +4,9 @@ import "lmika.dev/lmika/hugo-crm/models"
|
||||||
|
|
||||||
var themes = map[string]models.ThemeMeta{
|
var themes = map[string]models.ThemeMeta{
|
||||||
"bear": models.ThemeMeta{
|
"bear": models.ThemeMeta{
|
||||||
Name: "bear",
|
Name: "bear",
|
||||||
URL: "https://github.com/janraasch/hugo-bearblog",
|
URL: "https://github.com/janraasch/hugo-bearblog",
|
||||||
|
PreferTitle: true,
|
||||||
|
PostDir: "blog",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
47
services/posts/services.go
Normal file
47
services/posts/services.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package posts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"lmika.dev/lmika/hugo-crm/models"
|
||||||
|
"lmika.dev/lmika/hugo-crm/providers/db"
|
||||||
|
"lmika.dev/lmika/hugo-crm/services/jobs"
|
||||||
|
"lmika.dev/lmika/hugo-crm/services/sitebuilder"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
db *db.DB
|
||||||
|
sb *sitebuilder.Service
|
||||||
|
jobs *jobs.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
db *db.DB,
|
||||||
|
sb *sitebuilder.Service,
|
||||||
|
jobs *jobs.Service,
|
||||||
|
) *Service {
|
||||||
|
return &Service{
|
||||||
|
db: db,
|
||||||
|
sb: sb,
|
||||||
|
jobs: jobs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListPostOfSite(ctx context.Context, site models.Site) ([]models.Post, error) {
|
||||||
|
return s.db.ListPostsOfSite(ctx, site.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context, site models.Site, body string) (models.Post, error) {
|
||||||
|
post := models.Post{
|
||||||
|
SiteID: site.ID,
|
||||||
|
Body: body,
|
||||||
|
State: models.PostStatePublished,
|
||||||
|
PostDate: time.Now(),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := s.db.InsertPost(ctx, &post); err != nil {
|
||||||
|
return models.Post{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return post, s.jobs.Queue(ctx, s.sb.WritePost(site, post))
|
||||||
|
}
|
|
@ -3,11 +3,15 @@ package sitebuilder
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
"lmika.dev/lmika/hugo-crm/models"
|
"lmika.dev/lmika/hugo-crm/models"
|
||||||
"lmika.dev/lmika/hugo-crm/providers/git"
|
"lmika.dev/lmika/hugo-crm/providers/git"
|
||||||
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
"lmika.dev/lmika/hugo-crm/providers/hugo"
|
||||||
"lmika.dev/lmika/hugo-crm/providers/themes"
|
"lmika.dev/lmika/hugo-crm/providers/themes"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
@ -55,3 +59,57 @@ func (s *Service) CreateNewSite(site models.Site) models.Job {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) WritePost(site models.Site, post models.Post) models.Job {
|
||||||
|
return models.Job{
|
||||||
|
Do: func(ctx context.Context) error {
|
||||||
|
themeMeta, ok := s.themes.Lookup(site.Theme)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("theme not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
postFilename := filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.PostDir, post.CreatedAt.Format("2006-01-02-150405.md"))
|
||||||
|
|
||||||
|
log.Printf(" .. post %v", postFilename)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(postFilename), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
frontMatter := map[string]string{
|
||||||
|
"date": post.PostDate.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
if post.Title != "" {
|
||||||
|
frontMatter["title"] = post.Title
|
||||||
|
} else if themeMeta.PreferTitle {
|
||||||
|
frontMatter["title"] = post.PostDate.Format(time.ANSIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmBytes, err := yaml.Marshal(frontMatter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(postFilename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := f.WriteString("---\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := f.Write(fmBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := f.WriteString("---\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := f.WriteString(post.Body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
14
sql/queries/posts.sql
Normal file
14
sql/queries/posts.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-- name: ListPosts :many
|
||||||
|
SELECT * FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25;
|
||||||
|
|
||||||
|
-- name: InsertPost :one
|
||||||
|
INSERT INTO post (
|
||||||
|
site_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
state,
|
||||||
|
props,
|
||||||
|
post_date,
|
||||||
|
created_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING id;
|
|
@ -1,8 +1,26 @@
|
||||||
|
CREATE TYPE post_state AS ENUM (
|
||||||
|
'draft',
|
||||||
|
'published'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE site (
|
CREATE TABLE site (
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
theme TEXT NOT NULL,
|
theme TEXT NOT NULL,
|
||||||
props JSON NOT NULL
|
props JSON NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE post (
|
||||||
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
|
site_id BIGINT NOT NULL,
|
||||||
|
title TEXT,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
state post_state NOT NULL,
|
||||||
|
props JSON NOT NULL,
|
||||||
|
post_date TIMESTAMP WITH TIME ZONE,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (site_id) REFERENCES site(id)
|
||||||
|
);
|
|
@ -1 +0,0 @@
|
||||||
<h1>Site {{.site.Title}}</h1>
|
|
15
templates/sites/posts.html
Normal file
15
templates/sites/posts.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<h1>Site {{.site.Title}}</h1>
|
||||||
|
|
||||||
|
<form method="post" action="/sites/{{.site.ID}}/posts">
|
||||||
|
<textarea name="body"></textarea>
|
||||||
|
<input type="submit" value="Post">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{range .posts}}
|
||||||
|
<div class="post">
|
||||||
|
{{.Body}}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
{{else}}
|
||||||
|
<p>No posts yet</p>
|
||||||
|
{{end}}
|
Loading…
Reference in a new issue