feat: add category selection to post edit form and badges to post list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ffa86b12e9
commit
4c2ce7272d
|
|
@ -109,7 +109,7 @@ Starting weiro without any arguments will start the server.
|
||||||
|
|
||||||
ih := handlers.IndexHandler{SiteService: svcs.Sites}
|
ih := handlers.IndexHandler{SiteService: svcs.Sites}
|
||||||
lh := handlers.LoginHandler{Config: cfg, AuthService: svcs.Auth}
|
lh := handlers.LoginHandler{Config: cfg, AuthService: svcs.Auth}
|
||||||
ph := handlers.PostsHandler{PostService: svcs.Posts}
|
ph := handlers.PostsHandler{PostService: svcs.Posts, CategoryService: svcs.Categories}
|
||||||
uh := handlers.UploadsHandler{UploadsService: svcs.Uploads}
|
uh := handlers.UploadsHandler{UploadsService: svcs.Uploads}
|
||||||
ssh := handlers.SiteSettingsHandler{SiteService: svcs.Sites}
|
ssh := handlers.SiteSettingsHandler{SiteService: svcs.Sites}
|
||||||
ch := handlers.CategoriesHandler{CategoryService: svcs.Categories}
|
ch := handlers.CategoriesHandler{CategoryService: svcs.Categories}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ import (
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"lmika.dev/lmika/weiro/models"
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/services/categories"
|
||||||
"lmika.dev/lmika/weiro/services/posts"
|
"lmika.dev/lmika/weiro/services/posts"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PostsHandler struct {
|
type PostsHandler struct {
|
||||||
PostService *posts.Service
|
PostService *posts.Service
|
||||||
|
CategoryService *categories.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ph PostsHandler) Index(c fiber.Ctx) error {
|
func (ph PostsHandler) Index(c fiber.Ctx) error {
|
||||||
|
|
@ -42,8 +44,15 @@ func (ph PostsHandler) New(c fiber.Ctx) error {
|
||||||
State: models.StateDraft,
|
State: models.StateDraft,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cats, err := ph.CategoryService.ListCategories(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.Render("posts/edit", fiber.Map{
|
return c.Render("posts/edit", fiber.Map{
|
||||||
"post": p,
|
"post": p,
|
||||||
|
"categories": cats,
|
||||||
|
"selectedCategories": map[int64]bool{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,11 +71,28 @@ func (ph PostsHandler) Edit(c fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cats, err := ph.CategoryService.ListCategories(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
postCats, err := ph.PostService.GetPostCategories(c.Context(), postID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCategories := make(map[int64]bool)
|
||||||
|
for _, pc := range postCats {
|
||||||
|
selectedCategories[pc.ID] = true
|
||||||
|
}
|
||||||
|
|
||||||
return accepts(c, json(func() any {
|
return accepts(c, json(func() any {
|
||||||
return post
|
return post
|
||||||
}), html(func(c fiber.Ctx) error {
|
}), html(func(c fiber.Ctx) error {
|
||||||
return c.Render("posts/edit", fiber.Map{
|
return c.Render("posts/edit", fiber.Map{
|
||||||
"post": post,
|
"post": post,
|
||||||
|
"categories": cats,
|
||||||
|
"selectedCategories": selectedCategories,
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreatePostParams struct {
|
type CreatePostParams struct {
|
||||||
GUID string `form:"guid" json:"guid"`
|
GUID string `form:"guid" json:"guid"`
|
||||||
Title string `form:"title" json:"title"`
|
Title string `form:"title" json:"title"`
|
||||||
Body string `form:"body" json:"body"`
|
Body string `form:"body" json:"body"`
|
||||||
Action string `form:"action" json:"action"`
|
Action string `form:"action" json:"action"`
|
||||||
|
CategoryIDs []int64 `form:"category_ids" json:"category_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdatePost(ctx context.Context, params CreatePostParams) (*models.Post, error) {
|
func (s *Service) UpdatePost(ctx context.Context, params CreatePostParams) (*models.Post, error) {
|
||||||
|
|
@ -53,7 +54,21 @@ func (s *Service) UpdatePost(ctx context.Context, params CreatePostParams) (*mod
|
||||||
// Leave unchanged
|
// Leave unchanged
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.SavePost(ctx, post); err != nil {
|
// Use a transaction for atomicity of post save + category reassignment
|
||||||
|
tx, err := s.db.BeginTx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
txDB := s.db.QueriesWithTx(tx)
|
||||||
|
if err := txDB.SavePost(ctx, post); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := txDB.SetPostCategories(ctx, post.ID, params.CategoryIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@ import (
|
||||||
"lmika.dev/lmika/weiro/providers/db"
|
"lmika.dev/lmika/weiro/providers/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) ListPosts(ctx context.Context, showDeleted bool) ([]*models.Post, error) {
|
type PostWithCategories struct {
|
||||||
|
*models.Post
|
||||||
|
Categories []*models.Category
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListPosts(ctx context.Context, showDeleted bool) ([]*PostWithCategories, error) {
|
||||||
site, ok := models.GetSite(ctx)
|
site, ok := models.GetSite(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, models.SiteRequiredError
|
return nil, models.SiteRequiredError
|
||||||
|
|
@ -21,7 +26,15 @@ func (s *Service) ListPosts(ctx context.Context, showDeleted bool) ([]*models.Po
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return posts, nil
|
result := make([]*PostWithCategories, len(posts))
|
||||||
|
for i, post := range posts {
|
||||||
|
cats, err := s.db.SelectCategoriesOfPost(ctx, post.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result[i] = &PostWithCategories{Post: post, Categories: cats}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetPost(ctx context.Context, pid int64) (*models.Post, error) {
|
func (s *Service) GetPost(ctx context.Context, pid int64) (*models.Post, error) {
|
||||||
|
|
@ -32,3 +45,7 @@ func (s *Service) GetPost(ctx context.Context, pid int64) (*models.Post, error)
|
||||||
|
|
||||||
return post, nil
|
return post, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetPostCategories(ctx context.Context, postID int64) ([]*models.Category, error) {
|
||||||
|
return s.db.SelectCategoriesOfPost(ctx, postID)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,41 @@
|
||||||
data-controller="postedit"
|
data-controller="postedit"
|
||||||
data-action="keydown.meta+s->postedit#save keydown.meta+enter->postedit#publish"
|
data-action="keydown.meta+s->postedit#save keydown.meta+enter->postedit#publish"
|
||||||
data-postedit-save-action-value="{{ if $isPublished }}Update{{ else }}Save Draft{{ end }}">
|
data-postedit-save-action-value="{{ if $isPublished }}Update{{ else }}Save Draft{{ end }}">
|
||||||
<input type="hidden" name="guid" value="{{ .post.GUID }}">
|
<div class="row">
|
||||||
<div class="mb-2">
|
<div class="col-md-9">
|
||||||
<input type="text" name="title" class="form-control" placeholder="Title" value="{{ .post.Title }}">
|
<input type="hidden" name="guid" value="{{ .post.GUID }}">
|
||||||
</div>
|
<div class="mb-2">
|
||||||
<div>
|
<input type="text" name="title" class="form-control" placeholder="Title" value="{{ .post.Title }}">
|
||||||
<textarea data-postedit-target="bodyTextEdit" name="body" class="form-control" rows="3">{{.post.Body}}</textarea>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<textarea data-postedit-target="bodyTextEdit" name="body" class="form-control" rows="3">{{.post.Body}}</textarea>
|
||||||
{{ if $isPublished }}
|
</div>
|
||||||
<input type="submit" name="action" class="btn btn-primary mt-2" value="Update">
|
<div>
|
||||||
{{ else }}
|
{{ if $isPublished }}
|
||||||
<input type="submit" name="action" class="btn btn-primary mt-2" value="Publish">
|
<input type="submit" name="action" class="btn btn-primary mt-2" value="Update">
|
||||||
<input type="submit" name="action" class="btn btn-secondary mt-2" value="Save Draft">
|
{{ else }}
|
||||||
{{ end }}
|
<input type="submit" name="action" class="btn btn-primary mt-2" value="Publish">
|
||||||
|
<input type="submit" name="action" class="btn btn-secondary mt-2" value="Save Draft">
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Categories</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{ range .categories }}
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="category_ids"
|
||||||
|
value="{{ .ID }}" id="cat-{{ .ID }}"
|
||||||
|
{{ if index $.selectedCategories .ID }}checked{{ end }}>
|
||||||
|
<label class="form-check-label" for="cat-{{ .ID }}">{{ .Name }}</label>
|
||||||
|
</div>
|
||||||
|
{{ else }}
|
||||||
|
<span class="text-muted">No categories yet.</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,14 @@
|
||||||
{{ if $p.Title }}<h4 class="mb-3">{{ $p.Title }}</h4>{{ end }}
|
{{ if $p.Title }}<h4 class="mb-3">{{ $p.Title }}</h4>{{ end }}
|
||||||
{{ markdown $p.Body $.site }}
|
{{ markdown $p.Body $.site }}
|
||||||
|
|
||||||
<div class="mb-3 d-flex align-items-center">
|
<div class="mb-3 d-flex align-items-center flex-wrap gap-1">
|
||||||
{{ if eq .State 1 }}
|
{{ if eq $p.State 1 }}
|
||||||
<span class="text-muted">{{ $.user.FormatTime .UpdatedAt }}</span> <span class="ms-3 badge text-primary-emphasis bg-primary-subtle border border-primary-subtle">Draft</span>
|
<span class="text-muted">{{ $.user.FormatTime $p.UpdatedAt }}</span> <span class="ms-3 badge text-primary-emphasis bg-primary-subtle border border-primary-subtle">Draft</span>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<span class="text-muted">{{ $.user.FormatTime .PublishedAt }}</span>
|
<span class="text-muted">{{ $.user.FormatTime $p.PublishedAt }}</span>
|
||||||
|
{{ end }}
|
||||||
|
{{ range $p.Categories }}
|
||||||
|
<span class="ms-1 badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">{{ .Name }}</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue