Add categories feature #3
|
|
@ -112,6 +112,7 @@ Starting weiro without any arguments will start the server.
|
|||
ph := handlers.PostsHandler{PostService: svcs.Posts}
|
||||
uh := handlers.UploadsHandler{UploadsService: svcs.Uploads}
|
||||
ssh := handlers.SiteSettingsHandler{SiteService: svcs.Sites}
|
||||
ch := handlers.CategoriesHandler{CategoryService: svcs.Categories}
|
||||
|
||||
app.Get("/login", lh.Login)
|
||||
app.Post("/login", lh.DoLogin)
|
||||
|
|
@ -141,6 +142,13 @@ Starting weiro without any arguments will start the server.
|
|||
siteGroup.Get("/settings", ssh.General)
|
||||
siteGroup.Post("/settings", ssh.UpdateGeneral)
|
||||
|
||||
siteGroup.Get("/categories", ch.Index)
|
||||
siteGroup.Get("/categories/new", ch.New)
|
||||
siteGroup.Get("/categories/:categoryID", ch.Edit)
|
||||
siteGroup.Post("/categories", ch.Create)
|
||||
siteGroup.Post("/categories/:categoryID", ch.Update)
|
||||
siteGroup.Post("/categories/:categoryID/delete", ch.Delete)
|
||||
|
||||
app.Get("/", middleware.OptionalUser(svcs.Auth), ih.Index)
|
||||
app.Get("/first-run", ih.FirstRun)
|
||||
app.Post("/first-run", ih.FirstRunSubmit)
|
||||
|
|
|
|||
101
handlers/categories.go
Normal file
101
handlers/categories.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/services/categories"
|
||||
)
|
||||
|
||||
type CategoriesHandler struct {
|
||||
CategoryService *categories.Service
|
||||
}
|
||||
|
||||
func (ch CategoriesHandler) Index(c fiber.Ctx) error {
|
||||
cats, err := ch.CategoryService.ListCategoriesWithCounts(c.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("categories/index", fiber.Map{
|
||||
"categories": cats,
|
||||
})
|
||||
}
|
||||
|
||||
func (ch CategoriesHandler) New(c fiber.Ctx) error {
|
||||
cat := models.Category{
|
||||
GUID: models.NewNanoID(),
|
||||
}
|
||||
return c.Render("categories/edit", fiber.Map{
|
||||
"category": cat,
|
||||
"isNew": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (ch CategoriesHandler) Edit(c fiber.Ctx) error {
|
||||
catID, err := strconv.ParseInt(c.Params("categoryID"), 10, 64)
|
||||
if err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
cat, err := ch.CategoryService.GetCategory(c.Context(), catID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("categories/edit", fiber.Map{
|
||||
"category": cat,
|
||||
"isNew": false,
|
||||
})
|
||||
}
|
||||
|
||||
func (ch CategoriesHandler) Create(c fiber.Ctx) error {
|
||||
var req categories.CreateCategoryParams
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := ch.CategoryService.CreateCategory(c.Context(), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site := models.MustGetSite(c.Context())
|
||||
return c.Redirect().To(fmt.Sprintf("/sites/%v/categories", site.ID))
|
||||
}
|
||||
|
||||
func (ch CategoriesHandler) Update(c fiber.Ctx) error {
|
||||
catID, err := strconv.ParseInt(c.Params("categoryID"), 10, 64)
|
||||
if err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
var req categories.CreateCategoryParams
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = ch.CategoryService.UpdateCategory(c.Context(), catID, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site := models.MustGetSite(c.Context())
|
||||
return c.Redirect().To(fmt.Sprintf("/sites/%v/categories", site.ID))
|
||||
}
|
||||
|
||||
func (ch CategoriesHandler) Delete(c fiber.Ctx) error {
|
||||
catID, err := strconv.ParseInt(c.Params("categoryID"), 10, 64)
|
||||
if err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
if err := ch.CategoryService.DeleteCategory(c.Context(), catID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site := models.MustGetSite(c.Context())
|
||||
return c.Redirect().To(fmt.Sprintf("/sites/%v/categories", site.ID))
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"lmika.dev/lmika/weiro/providers/db"
|
||||
"lmika.dev/lmika/weiro/providers/uploadfiles"
|
||||
"lmika.dev/lmika/weiro/services/auth"
|
||||
"lmika.dev/lmika/weiro/services/categories"
|
||||
"lmika.dev/lmika/weiro/services/posts"
|
||||
"lmika.dev/lmika/weiro/services/publisher"
|
||||
"lmika.dev/lmika/weiro/services/sites"
|
||||
|
|
@ -21,6 +22,7 @@ type Services struct {
|
|||
Posts *posts.Service
|
||||
Sites *sites.Service
|
||||
Uploads *uploads.Service
|
||||
Categories *categories.Service
|
||||
}
|
||||
|
||||
func New(cfg config.Config) (*Services, error) {
|
||||
|
|
@ -37,6 +39,7 @@ func New(cfg config.Config) (*Services, error) {
|
|||
postService := posts.New(dbp, publisherQueue)
|
||||
siteService := sites.New(dbp)
|
||||
uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending"))
|
||||
categoriesService := categories.New(dbp, publisherQueue)
|
||||
|
||||
return &Services{
|
||||
DB: dbp,
|
||||
|
|
@ -46,6 +49,7 @@ func New(cfg config.Config) (*Services, error) {
|
|||
Posts: postService,
|
||||
Sites: siteService,
|
||||
Uploads: uploadService,
|
||||
Categories: categoriesService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/posts">Posts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/categories">Categories</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/uploads">Uploads</a>
|
||||
</li>
|
||||
|
|
|
|||
47
views/categories/edit.html
Normal file
47
views/categories/edit.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<main class="container">
|
||||
<div class="my-4">
|
||||
<h4>{{ if .isNew }}New Category{{ else }}Edit Category{{ end }}</h4>
|
||||
</div>
|
||||
|
||||
{{ if .isNew }}
|
||||
<form method="post" action="/sites/{{ .site.ID }}/categories">
|
||||
{{ else }}
|
||||
<form method="post" action="/sites/{{ .site.ID }}/categories/{{ .category.ID }}">
|
||||
{{ end }}
|
||||
<input type="hidden" name="guid" value="{{ .category.GUID }}">
|
||||
<div class="row mb-3">
|
||||
<label for="catName" class="col-sm-2 col-form-label">Name</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" id="catName" name="name" value="{{ .category.Name }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label for="catSlug" class="col-sm-2 col-form-label">Slug</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" id="catSlug" name="slug" value="{{ .category.Slug }}">
|
||||
<div class="form-text">Auto-generated from name if left blank.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label for="catDesc" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-9">
|
||||
<textarea class="form-control" id="catDesc" name="description" rows="5">{{ .category.Description }}</textarea>
|
||||
<div class="form-text">Markdown supported. Displayed on the category archive page.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-9">
|
||||
<button type="submit" class="btn btn-primary">{{ if .isNew }}Create{{ else }}Save{{ end }}</button>
|
||||
{{ if not .isNew }}
|
||||
<button type="button" class="btn btn-outline-danger ms-2"
|
||||
onclick="if(confirm('Delete this category? Posts will not be deleted.')) { document.getElementById('delete-form').submit(); }">Delete</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{ if not .isNew }}
|
||||
<form id="delete-form" method="post" action="/sites/{{ .site.ID }}/categories/{{ .category.ID }}/delete" style="display:none;"></form>
|
||||
{{ end }}
|
||||
</main>
|
||||
35
views/categories/index.html
Normal file
35
views/categories/index.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<main class="container">
|
||||
<div class="my-4 d-flex justify-content-between align-items-baseline">
|
||||
<h4>Categories</h4>
|
||||
<div>
|
||||
<a href="/sites/{{ .site.ID }}/categories/new" class="btn btn-success">New Category</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Posts</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .categories }}
|
||||
<tr>
|
||||
<td><a href="/sites/{{ $.site.ID }}/categories/{{ .ID }}">{{ .Name }}</a></td>
|
||||
<td><code>{{ .Slug }}</code></td>
|
||||
<td>{{ .PostCount }}</td>
|
||||
<td>
|
||||
<a href="/sites/{{ $.site.ID }}/categories/{{ .ID }}" class="btn btn-outline-secondary btn-sm">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">No categories yet.</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
Loading…
Reference in a new issue