feat(tag): add tag editing
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 1m13s
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 1m13s
This commit is contained in:
19
arch.gv
19
arch.gv
@@ -1,11 +1,24 @@
|
|||||||
digraph {
|
digraph {
|
||||||
Budget
|
|
||||||
RecurringCost
|
|
||||||
SavingGoal
|
|
||||||
Tag
|
Tag
|
||||||
Transaction
|
Transaction
|
||||||
Account
|
Account
|
||||||
BankConnection
|
BankConnection
|
||||||
|
Analytics
|
||||||
|
|
||||||
|
// Buckets
|
||||||
|
Budget
|
||||||
|
RecurringCost
|
||||||
|
SavingGoal
|
||||||
|
|
||||||
|
|
||||||
|
// Analytics -> {
|
||||||
|
// Budget
|
||||||
|
// RecurringCost
|
||||||
|
// SavingGoal
|
||||||
|
// Tag
|
||||||
|
// Transaction
|
||||||
|
// Account
|
||||||
|
// } [label="uses"]
|
||||||
|
|
||||||
BankConnection -> Transaction [label="imports into"]
|
BankConnection -> Transaction [label="imports into"]
|
||||||
BankConnection -> Account [label="references"]
|
BankConnection -> Account [label="references"]
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func (s ServiceImpl) GetAll(ctx context.Context, user *auth_types.User) ([]Budge
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s ServiceImpl) isBudgetValid(budget Budget) bool {
|
func (s ServiceImpl) isBudgetValid(budget Budget) bool {
|
||||||
err := core.ValidateString(budget.Name, "description")
|
err := core.ValidateString(budget.Name, "name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ templ navigation(path string) {
|
|||||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/treasurechest")) } href="/treasurechest">Treasure Chest</a>
|
<a class={ layoutLinkClass(strings.HasPrefix(path, "/treasurechest")) } href="/treasurechest">Treasure Chest</a>
|
||||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/account")) } href="/account">Account</a>
|
<a class={ layoutLinkClass(strings.HasPrefix(path, "/account")) } href="/account">Account</a>
|
||||||
<a class={ layoutLinkClass(strings.HasPrefix(path, "/budget")) } href="/budget">Budget</a>
|
<a class={ layoutLinkClass(strings.HasPrefix(path, "/budget")) } href="/budget">Budget</a>
|
||||||
|
<a class={ layoutLinkClass(strings.HasPrefix(path, "/tag")) } href="/tag">Tag</a>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"spend-sparrow/internal/dashboard"
|
"spend-sparrow/internal/dashboard"
|
||||||
"spend-sparrow/internal/handler"
|
"spend-sparrow/internal/handler"
|
||||||
"spend-sparrow/internal/handler/middleware"
|
"spend-sparrow/internal/handler/middleware"
|
||||||
|
"spend-sparrow/internal/tag"
|
||||||
"spend-sparrow/internal/transaction"
|
"spend-sparrow/internal/transaction"
|
||||||
"spend-sparrow/internal/transaction_recurring"
|
"spend-sparrow/internal/transaction_recurring"
|
||||||
"spend-sparrow/internal/treasure_chest"
|
"spend-sparrow/internal/treasure_chest"
|
||||||
@@ -111,6 +112,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
|
|
||||||
authDb := authentication.NewDbSqlite(d)
|
authDb := authentication.NewDbSqlite(d)
|
||||||
budgetDb := budget.NewDbSqlite(d)
|
budgetDb := budget.NewDbSqlite(d)
|
||||||
|
tagDb := tag.NewDbSqlite(d)
|
||||||
|
|
||||||
randomService := core.NewRandom()
|
randomService := core.NewRandom()
|
||||||
clockService := core.NewClock()
|
clockService := core.NewClock()
|
||||||
@@ -123,6 +125,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
transactionRecurringService := transaction_recurring.NewService(d, randomService, clockService)
|
transactionRecurringService := transaction_recurring.NewService(d, randomService, clockService)
|
||||||
dashboardService := dashboard.NewService(d)
|
dashboardService := dashboard.NewService(d)
|
||||||
budgetService := budget.NewService(budgetDb, randomService, clockService)
|
budgetService := budget.NewService(budgetDb, randomService, clockService)
|
||||||
|
tagService := tag.NewService(tagDb, randomService, clockService)
|
||||||
|
|
||||||
render := core.NewRender()
|
render := core.NewRender()
|
||||||
indexHandler := handler.NewIndex(render, clockService)
|
indexHandler := handler.NewIndex(render, clockService)
|
||||||
@@ -133,6 +136,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
transactionHandler := transaction.NewHandler(transactionService, accountService, treasureChestService, render)
|
transactionHandler := transaction.NewHandler(transactionService, accountService, treasureChestService, render)
|
||||||
transactionRecurringHandler := transaction_recurring.NewHandler(transactionRecurringService, render)
|
transactionRecurringHandler := transaction_recurring.NewHandler(transactionRecurringService, render)
|
||||||
budgetHandler := budget.NewHandler(budgetService, render)
|
budgetHandler := budget.NewHandler(budgetService, render)
|
||||||
|
tagHandler := tag.NewHandler(tagService, render)
|
||||||
|
|
||||||
go dailyTaskTimer(ctx, transactionService, authService)
|
go dailyTaskTimer(ctx, transactionService, authService)
|
||||||
|
|
||||||
@@ -144,6 +148,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
transactionHandler.Handle(router)
|
transactionHandler.Handle(router)
|
||||||
transactionRecurringHandler.Handle(router)
|
transactionRecurringHandler.Handle(router)
|
||||||
budgetHandler.Handle(router)
|
budgetHandler.Handle(router)
|
||||||
|
tagHandler.Handle(router)
|
||||||
|
|
||||||
// Serve static files (CSS, JS and images)
|
// Serve static files (CSS, JS and images)
|
||||||
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
||||||
|
|||||||
96
internal/tag/db.go
Normal file
96
internal/tag/db.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package tag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"spend-sparrow/internal/core"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Db interface {
|
||||||
|
Insert(ctx context.Context, tag Tag) (*Tag, error)
|
||||||
|
Update(ctx context.Context, tag Tag) (*Tag, error)
|
||||||
|
Delete(ctx context.Context, userId uuid.UUID, id uuid.UUID) error
|
||||||
|
Get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Tag, error)
|
||||||
|
GetAll(ctx context.Context, userId uuid.UUID) ([]Tag, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DbSqlite struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDbSqlite(db *sqlx.DB) *DbSqlite {
|
||||||
|
return &DbSqlite{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DbSqlite) Insert(ctx context.Context, tag Tag) (*Tag, error) {
|
||||||
|
r, err := db.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO tag (id, user_id, name, created_at, created_by, updated_at, updated_by)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
tag.Id, tag.UserId, tag.Name, tag.CreatedAt, tag.CreatedBy, tag.UpdatedAt, tag.UpdatedBy,
|
||||||
|
)
|
||||||
|
err = core.TransformAndLogDbError(ctx, "tag", r, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Get(ctx, tag.UserId, tag.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DbSqlite) Update(ctx context.Context, tag Tag) (*Tag, error) {
|
||||||
|
_, err := db.db.ExecContext(ctx, `
|
||||||
|
UPDATE tag
|
||||||
|
SET name = ?,
|
||||||
|
updated_at = ?,
|
||||||
|
updated_by = ?
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND id = ?`,
|
||||||
|
tag.Name, tag.UpdatedAt, tag.UpdatedBy, tag.UserId, tag.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "SQL error UpdateUser", "err", err)
|
||||||
|
return nil, core.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Get(ctx, tag.UserId, tag.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DbSqlite) Delete(ctx context.Context, userId uuid.UUID, id uuid.UUID) error {
|
||||||
|
r, err := db.db.ExecContext(
|
||||||
|
ctx,
|
||||||
|
"DELETE FROM tag WHERE user_id = ? AND id = ?",
|
||||||
|
userId,
|
||||||
|
id)
|
||||||
|
err = core.TransformAndLogDbError(ctx, "tag", r, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DbSqlite) Get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Tag, error) {
|
||||||
|
var tag Tag
|
||||||
|
err := db.db.Get(&tag, "SELECT * FROM tag WHERE id = ? AND user_id = ?", id, userId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "Could not get tag", "err", err)
|
||||||
|
return nil, core.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DbSqlite) GetAll(ctx context.Context, userId uuid.UUID) ([]Tag, error) {
|
||||||
|
var tags []Tag
|
||||||
|
err := db.db.Select(&tags, "SELECT * FROM tag WHERE user_id = ?", userId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.ErrorContext(ctx, "Could not GetAll tag", "err", err)
|
||||||
|
return nil, core.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
179
internal/tag/handler.go
Normal file
179
internal/tag/handler.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package tag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"spend-sparrow/internal/core"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DECIMALS_MULTIPLIER = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
Handle(router *http.ServeMux)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandlerImpl struct {
|
||||||
|
s Service
|
||||||
|
r *core.Render
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(s Service, r *core.Render) Handler {
|
||||||
|
return HandlerImpl{
|
||||||
|
s: s,
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HandlerImpl) Handle(r *http.ServeMux) {
|
||||||
|
r.Handle("GET /tag", h.handlePage())
|
||||||
|
r.Handle("GET /tag/new", h.handleNew())
|
||||||
|
r.Handle("GET /tag/{id}", h.handleEdit())
|
||||||
|
r.Handle("POST /tag/{id}", h.handlePost())
|
||||||
|
r.Handle("DELETE /tag/{id}", h.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HandlerImpl) handlePage() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
|
user := core.GetUser(r)
|
||||||
|
if user == nil {
|
||||||
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err := h.s.GetAll(r.Context(), user)
|
||||||
|
if err != nil {
|
||||||
|
h.r.RenderLayout(r, w, core.ErrorComp(err), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comp := page(tags)
|
||||||
|
h.r.RenderLayout(r, w, comp, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HandlerImpl) handleNew() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
|
user := core.GetUser(r)
|
||||||
|
if user == nil {
|
||||||
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comp := editNew()
|
||||||
|
h.r.RenderLayout(r, w, comp, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HandlerImpl) handleEdit() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
|
user := core.GetUser(r)
|
||||||
|
if user == nil {
|
||||||
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, err := h.s.Get(r.Context(), user, id)
|
||||||
|
if err != nil {
|
||||||
|
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comp := edit(*tag)
|
||||||
|
h.r.RenderLayout(r, w, comp, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HandlerImpl) handlePost() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
|
user := core.GetUser(r)
|
||||||
|
if user == nil {
|
||||||
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
id uuid.UUID
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
idStr := r.PathValue("id")
|
||||||
|
if idStr != "new" {
|
||||||
|
id, err = uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
core.HandleError(w, r, fmt.Errorf("could not parse value: %w", core.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input := Tag{
|
||||||
|
Id: id,
|
||||||
|
Name: r.FormValue("name"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if idStr == "new" {
|
||||||
|
_, err = h.s.Add(r.Context(), user, input)
|
||||||
|
if err != nil {
|
||||||
|
core.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = h.s.Update(r.Context(), user, input)
|
||||||
|
if err != nil {
|
||||||
|
core.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.DoRedirect(w, r, "/tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HandlerImpl) handleDelete() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
core.UpdateSpan(r)
|
||||||
|
|
||||||
|
user := core.GetUser(r)
|
||||||
|
if user == nil {
|
||||||
|
core.DoRedirect(w, r, "/auth/signin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := r.PathValue("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
core.HandleError(w, r, fmt.Errorf("could not parse Id: %w", core.ErrBadRequest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.s.Delete(r.Context(), user, id)
|
||||||
|
if err != nil {
|
||||||
|
core.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.DoRedirect(w, r, "/tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
115
internal/tag/service.go
Normal file
115
internal/tag/service.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package tag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"spend-sparrow/internal/auth_types"
|
||||||
|
"spend-sparrow/internal/core"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Add(ctx context.Context, user *auth_types.User, tag Tag) (*Tag, error)
|
||||||
|
Update(ctx context.Context, user *auth_types.User, tag Tag) (*Tag, error)
|
||||||
|
Delete(ctx context.Context, user *auth_types.User, tagId uuid.UUID) error
|
||||||
|
Get(ctx context.Context, user *auth_types.User, tagId uuid.UUID) (*Tag, error)
|
||||||
|
GetAll(ctx context.Context, user *auth_types.User) ([]Tag, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceImpl struct {
|
||||||
|
db Db
|
||||||
|
clock core.Clock
|
||||||
|
random core.Random
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(db Db, random core.Random, clock core.Clock) Service {
|
||||||
|
return ServiceImpl{
|
||||||
|
db: db,
|
||||||
|
clock: clock,
|
||||||
|
random: random,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ServiceImpl) Add(ctx context.Context, user *auth_types.User, tag Tag) (*Tag, error) {
|
||||||
|
if user == nil {
|
||||||
|
return nil, core.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid := s.isTagValid(tag)
|
||||||
|
if !isValid {
|
||||||
|
return nil, core.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
newId, err := s.random.UUID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.Id = newId
|
||||||
|
tag.UserId = user.Id
|
||||||
|
tag.CreatedBy = user.Id
|
||||||
|
tag.CreatedAt = s.clock.Now()
|
||||||
|
|
||||||
|
return s.db.Insert(ctx, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ServiceImpl) Update(ctx context.Context, user *auth_types.User, input Tag) (*Tag, error) {
|
||||||
|
if user == nil {
|
||||||
|
return nil, core.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, err := s.Get(ctx, user, input.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.Name = input.Name
|
||||||
|
|
||||||
|
if user.Id != tag.UserId {
|
||||||
|
return nil, core.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid := s.isTagValid(*tag)
|
||||||
|
if !isValid {
|
||||||
|
return nil, core.ErrBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.UpdatedBy = &user.Id
|
||||||
|
now := s.clock.Now()
|
||||||
|
tag.UpdatedAt = &now
|
||||||
|
|
||||||
|
return s.db.Update(ctx, *tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ServiceImpl) Delete(ctx context.Context, user *auth_types.User, tagId uuid.UUID) error {
|
||||||
|
if user == nil {
|
||||||
|
return core.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.Delete(ctx, user.Id, tagId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ServiceImpl) Get(ctx context.Context, user *auth_types.User, tagId uuid.UUID) (*Tag, error) {
|
||||||
|
if user == nil {
|
||||||
|
return nil, core.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.Get(ctx, user.Id, tagId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ServiceImpl) GetAll(ctx context.Context, user *auth_types.User) ([]Tag, error) {
|
||||||
|
if user == nil {
|
||||||
|
return nil, core.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.GetAll(ctx, user.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ServiceImpl) isTagValid(tag Tag) bool {
|
||||||
|
err := core.ValidateString(tag.Name, "name")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
119
internal/tag/template.templ
Normal file
119
internal/tag/template.templ
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package tag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"spend-sparrow/internal/core"
|
||||||
|
"spend-sparrow/internal/template/svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ page(tags []Tag) {
|
||||||
|
@core.Breadcrumb([]string{"Home", "Tag"}, []string{"/", "/tag"})
|
||||||
|
<div class="flex flex-wrap gap-20 text-xl mt-10 justify-center">
|
||||||
|
@newItem()
|
||||||
|
for _,tag:=range(tags ) {
|
||||||
|
@item(tag)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ editNew() {
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
@core.Breadcrumb([]string{"Home", "Tag", "New"}, []string{"/", "/tag", "/tag/new"})
|
||||||
|
<div class="flex justify-center items-center flex-1">
|
||||||
|
<form
|
||||||
|
hx-post={ "/tag/new" }
|
||||||
|
class="grid grid-cols-4 items-center gap-4 max-w-2xl"
|
||||||
|
>
|
||||||
|
<label for="timestamp" class="text-sm text-gray-500">Name</label>
|
||||||
|
<input
|
||||||
|
autofocus
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
class="bg-white input datetime col-span-3"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-6 justify-end col-span-4">
|
||||||
|
<a href="/tag" class="col-start-3 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||||
|
<span class="h-4 w-4">
|
||||||
|
@svg.Cancel()
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Cancel
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||||
|
@svg.Save()
|
||||||
|
<span>
|
||||||
|
Save
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ edit(tag Tag) {
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
@core.Breadcrumb([]string{"Home", "Tag", tag.Name}, []string{"/", "/tag", "/tag/" + tag.Id.String()})
|
||||||
|
<div class="flex justify-center items-center flex-1">
|
||||||
|
<form
|
||||||
|
hx-post={ "/tag/" + tag.Id.String() }
|
||||||
|
class="grid grid-cols-4 items-center gap-4 max-w-2xl"
|
||||||
|
>
|
||||||
|
<label for="timestamp" class="text-sm text-gray-500">Name</label>
|
||||||
|
<input
|
||||||
|
autofocus
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={ tag.Name }
|
||||||
|
class="bg-white input datetime col-span-3"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row-reverse gap-6 justify-end col-span-4">
|
||||||
|
<button type="submit" class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||||
|
@svg.Save()
|
||||||
|
<span>
|
||||||
|
Save
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<a href="/tag" class="col-start-3 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg hover:underline flex items-center gap-2 justify-center">
|
||||||
|
<span class="h-4 w-4">
|
||||||
|
@svg.Cancel()
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Cancel
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
hx-delete={ "/tag/" + tag.Id.String() }
|
||||||
|
hx-confirm={ "Do you really want to delete '" + tag.Name + "'" }
|
||||||
|
class="col-start-4 p-2 px-4 decoration-yellow-400 decoration-[0.25rem] hover:bg-red-50 rounded-lg hover:underline flex items-center gap-2 justify-center"
|
||||||
|
>
|
||||||
|
@svg.Delete()
|
||||||
|
<span>
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ newItem() {
|
||||||
|
<a
|
||||||
|
href="/tag/new"
|
||||||
|
class="p-5 w-64 h-64 flex gap-10 active:bg-gray-200 flex-col justify-center items-center hover:bg-gray-100 transition-all cursor-pointer rounded-lg bg-gray-50 border-1 border-gray-300"
|
||||||
|
>
|
||||||
|
New Tag
|
||||||
|
<div class="w-10">
|
||||||
|
@svg.Plus()
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ item(tag Tag) {
|
||||||
|
<a href={ "/tag/" + tag.Id.String() } class="p-5 w-64 h-64 flex gap-10 active:bg-gray-200 flex-col justify-center items-center hover:bg-gray-100 transition-all cursor-pointer rounded-lg bg-gray-50 border-1 border-gray-300">
|
||||||
|
<span>
|
||||||
|
{ tag.Name }
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
19
internal/tag/types.go
Normal file
19
internal/tag/types.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package tag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
Id uuid.UUID `db:"id"`
|
||||||
|
UserId uuid.UUID `db:"user_id"`
|
||||||
|
|
||||||
|
Name string `db:"name"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
CreatedBy uuid.UUID `db:"created_by"`
|
||||||
|
UpdatedAt *time.Time `db:"updated_at"`
|
||||||
|
UpdatedBy *uuid.UUID `db:"updated_by"`
|
||||||
|
}
|
||||||
13
migration/012_tag.up.sql
Normal file
13
migration/012_tag.up.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
CREATE TABLE "tag" (
|
||||||
|
id TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
created_by TEXT NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
updated_by TEXT
|
||||||
|
) WITHOUT ROWID;
|
||||||
|
|
||||||
Reference in New Issue
Block a user