Initial commit of admin panel.
This commit is contained in:
parent
c8db468292
commit
33e98f9a9e
8 changed files with 466 additions and 7 deletions
2
go.mod
2
go.mod
|
|
@ -1,6 +1,6 @@
|
||||||
module github.com/mikestefanello/pagoda
|
module github.com/mikestefanello/pagoda
|
||||||
|
|
||||||
go 1.23.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
entgo.io/ent v0.14.3
|
entgo.io/ent v0.14.3
|
||||||
|
|
|
||||||
284
pkg/handlers/admin.go
Normal file
284
pkg/handlers/admin.go
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"entgo.io/ent/dialect/sql"
|
||||||
|
"entgo.io/ent/entc/gen"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/pagoda/ent"
|
||||||
|
"github.com/mikestefanello/pagoda/ent/passwordtoken"
|
||||||
|
"github.com/mikestefanello/pagoda/ent/user"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/pager"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/redirect"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/ui/pages"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO plugins should create keys dynamically
|
||||||
|
const entityContextKey = "admin:entity"
|
||||||
|
const entityIDContextKey = "admin:entity_id"
|
||||||
|
|
||||||
|
type Admin struct {
|
||||||
|
orm *ent.Client
|
||||||
|
graph *gen.Graph
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(new(Admin))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) Init(c *services.Container) error {
|
||||||
|
h.graph = c.Graph
|
||||||
|
h.orm = c.ORM
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) Routes(g *echo.Group) {
|
||||||
|
// TODO admin user status middleware
|
||||||
|
entities := g.Group("/admin/content")
|
||||||
|
|
||||||
|
for _, p := range h.getEntityPlugins() {
|
||||||
|
pg := entities.Group(fmt.Sprintf("/%s", p.ID))
|
||||||
|
pg.GET("", h.EntityList(p)).Name = p.RouteNameList()
|
||||||
|
pg.POST("", h.EntityList(p)).Name = p.RouteNameListSubmit()
|
||||||
|
pg.GET("/add", h.EntityAdd(p)).Name = p.RouteNameAdd()
|
||||||
|
pg.POST("/add", h.EntityAddSubmit(p)).Name = p.RouteNameAddSubmit()
|
||||||
|
pg.GET("/:id/edit", h.EntityEdit(p), h.entityPluginMiddleware(p)).Name = p.RouteNameEdit()
|
||||||
|
pg.POST("/:id/edit", h.EntityEditSubmit(p), h.entityPluginMiddleware(p)).Name = p.RouteNameEditSubmit()
|
||||||
|
pg.GET("/:id/delete", h.EntityDelete(p), h.entityPluginMiddleware(p)).Name = p.RouteNameDelete()
|
||||||
|
pg.POST("/:id/delete", h.EntityDeleteSubmit(p), h.entityPluginMiddleware(p)).Name = p.RouteNameDeleteSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO, maybe this can be used outside of admin stuff as well?
|
||||||
|
func (h *Admin) entityPluginMiddleware(plugin AdminEntityPlugin) echo.MiddlewareFunc {
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
id, err := strconv.Atoi(ctx.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "invalid entity ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
entity, err := plugin.Load(ctx, h.orm, id)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
ctx.Set(entityIDContextKey, id)
|
||||||
|
ctx.Set(entityContextKey, entity)
|
||||||
|
return next(ctx)
|
||||||
|
case errors.Is(err, new(ent.NotFoundError)):
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "entity not found")
|
||||||
|
default:
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) EntityList(p AdminEntityPlugin) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
var err error
|
||||||
|
pgr := pager.NewPager(ctx, 25)
|
||||||
|
params := pages.AdminEntityListParams{
|
||||||
|
Title: p.LabelPlural,
|
||||||
|
Headers: p.Heading,
|
||||||
|
EditRoute: p.RouteNameEdit(), // todo remove, pass in plugin
|
||||||
|
DeleteRoute: p.RouteNameDelete(), // todo remove, pass in plugin
|
||||||
|
Pager: pgr,
|
||||||
|
}
|
||||||
|
params.Rows, err = p.List(ctx, h.orm, pgr)
|
||||||
|
if err != nil {
|
||||||
|
return fail(err, fmt.Sprintf("failed to query %s", p.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages.AdminEntityList(ctx, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) EntityAdd(p AdminEntityPlugin) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) EntityAddSubmit(p AdminEntityPlugin) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) EntityEdit(p AdminEntityPlugin) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) EntityEditSubmit(p AdminEntityPlugin) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) EntityDelete(p AdminEntityPlugin) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
return pages.AdminEntityDelete(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) EntityDeleteSubmit(p AdminEntityPlugin) echo.HandlerFunc {
|
||||||
|
return func(ctx echo.Context) error {
|
||||||
|
id := ctx.Get(entityIDContextKey).(int)
|
||||||
|
if err := p.Delete(ctx, h.orm, id); err != nil {
|
||||||
|
return fail(err, fmt.Sprintf("failed to delete %s (ID: %d)", p.ID, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Success(ctx, fmt.Sprintf("Successfully deleted %s.", strings.ToLower(p.Label)))
|
||||||
|
|
||||||
|
return redirect.
|
||||||
|
New(ctx).
|
||||||
|
Route(p.RouteNameList()).
|
||||||
|
Go()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO inject orm? move to separate package?
|
||||||
|
type AdminEntityPlugin struct {
|
||||||
|
ID string
|
||||||
|
Label string
|
||||||
|
LabelPlural string
|
||||||
|
Heading []string
|
||||||
|
List func(ctx echo.Context, orm *ent.Client, pgr pager.Pager) ([]pages.AdminEntityListRow, error)
|
||||||
|
Load func(ctx echo.Context, orm *ent.Client, id int) (any, error)
|
||||||
|
Delete func(ctx echo.Context, orm *ent.Client, id int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdminEntityPlugin) RouteNameList() string {
|
||||||
|
return fmt.Sprintf("admin:%s_list", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdminEntityPlugin) RouteNameListSubmit() string {
|
||||||
|
return fmt.Sprintf("admin:%s_list.submit", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdminEntityPlugin) RouteNameAdd() string {
|
||||||
|
return fmt.Sprintf("admin:%s_add", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdminEntityPlugin) RouteNameEdit() string {
|
||||||
|
return fmt.Sprintf("admin:%s_edit", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdminEntityPlugin) RouteNameDelete() string {
|
||||||
|
return fmt.Sprintf("admin:%s_delete", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdminEntityPlugin) RouteNameAddSubmit() string {
|
||||||
|
return fmt.Sprintf("admin:%s_add.submit", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdminEntityPlugin) RouteNameEditSubmit() string {
|
||||||
|
return fmt.Sprintf("admin:%s_edit.submit", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AdminEntityPlugin) RouteNameDeleteSubmit() string {
|
||||||
|
return fmt.Sprintf("admin:%s_delete.submit", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Admin) getEntityPlugins() []AdminEntityPlugin {
|
||||||
|
return []AdminEntityPlugin{
|
||||||
|
{
|
||||||
|
ID: "user",
|
||||||
|
Label: "User",
|
||||||
|
LabelPlural: "Users",
|
||||||
|
Heading: []string{
|
||||||
|
"ID",
|
||||||
|
"Name",
|
||||||
|
"Email",
|
||||||
|
"Created at",
|
||||||
|
},
|
||||||
|
List: func(ctx echo.Context, client *ent.Client, pgr pager.Pager) ([]pages.AdminEntityListRow, error) {
|
||||||
|
users, err := client.User.
|
||||||
|
Query().
|
||||||
|
Limit(pgr.ItemsPerPage).
|
||||||
|
Offset(pgr.GetOffset()).
|
||||||
|
Order(user.ByCreatedAt(sql.OrderDesc())).
|
||||||
|
All(ctx.Request().Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]pages.AdminEntityListRow, 0, len(users))
|
||||||
|
|
||||||
|
for _, u := range users {
|
||||||
|
rows = append(rows, pages.AdminEntityListRow{
|
||||||
|
ID: u.ID,
|
||||||
|
Columns: []string{
|
||||||
|
fmt.Sprint(u.ID),
|
||||||
|
u.Name,
|
||||||
|
u.Email,
|
||||||
|
u.CreatedAt.Format(time.RFC822),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
},
|
||||||
|
Load: func(ctx echo.Context, orm *ent.Client, id int) (any, error) {
|
||||||
|
return orm.User.Get(ctx.Request().Context(), id)
|
||||||
|
},
|
||||||
|
Delete: func(ctx echo.Context, orm *ent.Client, id int) error {
|
||||||
|
return orm.User.DeleteOneID(id).Exec(ctx.Request().Context())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "passwordtoken",
|
||||||
|
Label: "Password token",
|
||||||
|
LabelPlural: "Password tokens",
|
||||||
|
Heading: []string{
|
||||||
|
"ID",
|
||||||
|
"Hash",
|
||||||
|
"Created at",
|
||||||
|
},
|
||||||
|
List: func(ctx echo.Context, client *ent.Client, pgr pager.Pager) ([]pages.AdminEntityListRow, error) {
|
||||||
|
tokens, err := client.PasswordToken.
|
||||||
|
Query().
|
||||||
|
Limit(pgr.ItemsPerPage).
|
||||||
|
Offset(pgr.GetOffset()).
|
||||||
|
Order(passwordtoken.ByCreatedAt(sql.OrderDesc())).
|
||||||
|
All(ctx.Request().Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]pages.AdminEntityListRow, 0, len(tokens))
|
||||||
|
|
||||||
|
for _, t := range tokens {
|
||||||
|
rows = append(rows, pages.AdminEntityListRow{
|
||||||
|
ID: t.ID,
|
||||||
|
Columns: []string{
|
||||||
|
fmt.Sprint(t.ID),
|
||||||
|
t.Hash,
|
||||||
|
t.CreatedAt.Format(time.RFC822),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
},
|
||||||
|
Load: func(ctx echo.Context, orm *ent.Client, id int) (any, error) {
|
||||||
|
return orm.PasswordToken.Get(ctx.Request().Context(), id)
|
||||||
|
},
|
||||||
|
Delete: func(ctx echo.Context, orm *ent.Client, id int) error {
|
||||||
|
return orm.PasswordToken.DeleteOneID(id).Exec(ctx.Request().Context())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
"github.com/mikestefanello/pagoda/pkg/log"
|
"github.com/mikestefanello/pagoda/pkg/log"
|
||||||
"github.com/mikestefanello/pagoda/pkg/msg"
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||||
"github.com/mikestefanello/pagoda/pkg/services"
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
@ -70,8 +71,7 @@ func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc
|
||||||
return next(c)
|
return next(c)
|
||||||
case services.InvalidPasswordTokenError:
|
case services.InvalidPasswordTokenError:
|
||||||
msg.Warning(c, "The link is either invalid or has expired. Please request a new one.")
|
msg.Warning(c, "The link is either invalid or has expired. Please request a new one.")
|
||||||
// TODO use the const for route name
|
return c.Redirect(http.StatusFound, c.Echo().Reverse(routenames.ForgotPassword))
|
||||||
return c.Redirect(http.StatusFound, c.Echo().Reverse("forgot_password"))
|
|
||||||
default:
|
default:
|
||||||
return echo.NewHTTPError(
|
return echo.NewHTTPError(
|
||||||
http.StatusInternalServerError,
|
http.StatusInternalServerError,
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ func (r *Redirect) Params(params ...any) *Redirect {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusCode sets the HTTP status code which defaults to http.StatusFound.
|
// StatusCode sets the HTTP status code which defaults to http.StatusTemporaryRedirect.
|
||||||
// Does not apply to HTMX redirects.
|
// Does not apply to HTMX redirects.
|
||||||
func (r *Redirect) StatusCode(code int) *Redirect {
|
func (r *Redirect) StatusCode(code int) *Redirect {
|
||||||
r.status = code
|
r.status = code
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,21 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mikestefanello/backlite"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
|
|
||||||
entsql "entgo.io/ent/dialect/sql"
|
entsql "entgo.io/ent/dialect/sql"
|
||||||
|
"entgo.io/ent/entc"
|
||||||
|
"entgo.io/ent/entc/gen"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/mikestefanello/backlite"
|
||||||
"github.com/mikestefanello/pagoda/config"
|
"github.com/mikestefanello/pagoda/config"
|
||||||
"github.com/mikestefanello/pagoda/ent"
|
"github.com/mikestefanello/pagoda/ent"
|
||||||
"github.com/mikestefanello/pagoda/pkg/log"
|
"github.com/mikestefanello/pagoda/pkg/log"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
// Required by ent.
|
// Required by ent.
|
||||||
_ "github.com/mikestefanello/pagoda/ent/runtime"
|
_ "github.com/mikestefanello/pagoda/ent/runtime"
|
||||||
|
|
@ -46,6 +50,9 @@ type Container struct {
|
||||||
// ORM stores a client to the ORM.
|
// ORM stores a client to the ORM.
|
||||||
ORM *ent.Client
|
ORM *ent.Client
|
||||||
|
|
||||||
|
// Graph is the entity graph defined by your Ent schema.
|
||||||
|
Graph *gen.Graph
|
||||||
|
|
||||||
// Mail stores an email sending client.
|
// Mail stores an email sending client.
|
||||||
Mail *MailClient
|
Mail *MailClient
|
||||||
|
|
||||||
|
|
@ -184,6 +191,16 @@ func (c *Container) initORM() {
|
||||||
if err := c.ORM.Schema.Create(context.Background()); err != nil {
|
if err := c.ORM.Schema.Create(context.Background()); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the graph.
|
||||||
|
_, b, _, _ := runtime.Caller(0)
|
||||||
|
d := path.Join(path.Dir(b))
|
||||||
|
p := filepath.Join(filepath.Dir(d), "../ent/schema")
|
||||||
|
g, err := entc.LoadGraph(p, &gen.Config{})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
c.Graph = g
|
||||||
}
|
}
|
||||||
|
|
||||||
// initAuth initializes the authentication client.
|
// initAuth initializes the authentication client.
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,9 @@ func TestNewContainer(t *testing.T) {
|
||||||
assert.NotNil(t, c.Mail)
|
assert.NotNil(t, c.Mail)
|
||||||
assert.NotNil(t, c.Auth)
|
assert.NotNil(t, c.Auth)
|
||||||
assert.NotNil(t, c.Tasks)
|
assert.NotNil(t, c.Tasks)
|
||||||
|
|
||||||
|
g := c.Graph
|
||||||
|
if g == nil {
|
||||||
|
//c.ORM.User.Create().
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
pkg/ui/layouts/admin.go
Normal file
65
pkg/ui/layouts/admin.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package layouts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/routenames"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||||
|
. "github.com/mikestefanello/pagoda/pkg/ui/components"
|
||||||
|
|
||||||
|
. "maragu.dev/gomponents"
|
||||||
|
. "maragu.dev/gomponents/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Admin(r *ui.Request, content Node) Node {
|
||||||
|
return Doctype(
|
||||||
|
HTML(
|
||||||
|
Lang("en"),
|
||||||
|
Head(
|
||||||
|
Metatags(r),
|
||||||
|
CSS(),
|
||||||
|
JS(r),
|
||||||
|
),
|
||||||
|
Body(
|
||||||
|
Div(
|
||||||
|
Class("box"),
|
||||||
|
Div(
|
||||||
|
Class("columns"),
|
||||||
|
Div(
|
||||||
|
Class("column is-2"),
|
||||||
|
adminMenu(r),
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Class("column is-10"),
|
||||||
|
If(len(r.Title) > 0, H1(Class("title"), Text(r.Title))),
|
||||||
|
FlashMessages(r),
|
||||||
|
content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminMenu(r *ui.Request) Node {
|
||||||
|
return Aside(
|
||||||
|
Class("menu"),
|
||||||
|
HxBoost(),
|
||||||
|
P(
|
||||||
|
Class("menu-label"),
|
||||||
|
Text("Content"),
|
||||||
|
),
|
||||||
|
Ul(
|
||||||
|
Class("menu-list"),
|
||||||
|
MenuLink(r, "Users", "admin:user_list"),
|
||||||
|
MenuLink(r, "Tokens", "admin:passwordtoken_list"),
|
||||||
|
),
|
||||||
|
P(
|
||||||
|
Class("menu-label"),
|
||||||
|
Text("Account"),
|
||||||
|
),
|
||||||
|
Ul(
|
||||||
|
Class("menu-list"),
|
||||||
|
If(r.IsAuth, MenuLink(r, "Logout", routenames.Logout)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
88
pkg/ui/pages/entity.go
Normal file
88
pkg/ui/pages/entity.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/pager"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/ui"
|
||||||
|
. "github.com/mikestefanello/pagoda/pkg/ui/components"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
||||||
|
. "maragu.dev/gomponents"
|
||||||
|
. "maragu.dev/gomponents/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Entity(ctx echo.Context) error {
|
||||||
|
return ui.NewRequest(ctx).Render(layouts.Admin, Div(Text("abc")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminEntityDelete(ctx echo.Context) error {
|
||||||
|
r := ui.NewRequest(ctx)
|
||||||
|
form := Form(
|
||||||
|
Method(http.MethodPost),
|
||||||
|
H2(Text("Are you sure you want to delete this entity?")),
|
||||||
|
ControlGroup(
|
||||||
|
FormButton("is-link", "Delete"),
|
||||||
|
ButtonLink("/", "is-secondary", "Cancel"),
|
||||||
|
),
|
||||||
|
CSRF(r),
|
||||||
|
)
|
||||||
|
|
||||||
|
return r.Render(layouts.Admin, form)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminEntityListParams struct {
|
||||||
|
Title string
|
||||||
|
Headers []string
|
||||||
|
Rows []AdminEntityListRow
|
||||||
|
EditRoute string
|
||||||
|
DeleteRoute string
|
||||||
|
Pager pager.Pager
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminEntityListRow struct {
|
||||||
|
ID int
|
||||||
|
Columns []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminEntityList(ctx echo.Context, params AdminEntityListParams) error {
|
||||||
|
r := ui.NewRequest(ctx)
|
||||||
|
r.Title = params.Title
|
||||||
|
|
||||||
|
genHeader := func() Node {
|
||||||
|
g := make(Group, 0, len(params.Headers)+2)
|
||||||
|
for _, h := range params.Headers {
|
||||||
|
g = append(g, Th(Text(h)))
|
||||||
|
}
|
||||||
|
g = append(g, Th(), Th())
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
genRow := func(row AdminEntityListRow) Node {
|
||||||
|
g := make(Group, 0, len(row.Columns)+2)
|
||||||
|
for _, h := range row.Columns {
|
||||||
|
g = append(g, Td(Text(h)))
|
||||||
|
}
|
||||||
|
g = append(g,
|
||||||
|
Td(A(Href(r.Path(params.EditRoute, row.ID)), Text("Edit"))),
|
||||||
|
Td(A(Href(r.Path(params.DeleteRoute, row.ID)), Text("Delete"))),
|
||||||
|
)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
genRows := func() Node {
|
||||||
|
g := make(Group, 0, len(params.Rows))
|
||||||
|
for _, row := range params.Rows {
|
||||||
|
g = append(g, Tr(genRow(row)))
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Render(layouts.Admin, Table(
|
||||||
|
Class("table"),
|
||||||
|
THead(
|
||||||
|
Tr(genHeader()),
|
||||||
|
),
|
||||||
|
TBody(genRows()),
|
||||||
|
))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue