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