feat(tag): draft for inline editing
Some checks failed
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Failing after 1m15s

This commit is contained in:
2026-01-08 18:54:18 +01:00
parent 5af5ab2a0c
commit a570c44d75
5 changed files with 89 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ package budget
import ( import (
"spend-sparrow/internal/core" "spend-sparrow/internal/core"
"spend-sparrow/internal/tag"
"spend-sparrow/internal/template/svg" "spend-sparrow/internal/template/svg"
) )
@@ -80,6 +81,8 @@ templ edit(budget Budget) {
value={ budget.Value / 100 } value={ budget.Value / 100 }
class="bg-white input col-span-3" class="bg-white input col-span-3"
/> />
<label for="tag" class="text-sm text-gray-500">Tags</label>
@tag.InlineEditInput("col-span-3")
<div class="flex flex-row-reverse gap-6 justify-end col-span-4"> <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"> <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() @svg.Save()

View File

@@ -15,6 +15,7 @@ type Db interface {
delete(ctx context.Context, userId uuid.UUID, id uuid.UUID) error delete(ctx context.Context, userId uuid.UUID, id uuid.UUID) error
get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Tag, error) get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Tag, error)
getAll(ctx context.Context, userId uuid.UUID) ([]Tag, error) getAll(ctx context.Context, userId uuid.UUID) ([]Tag, error)
find(ctx context.Context, userId uuid.UUID, search string) ([]Tag, error)
} }
type DbSqlite struct { type DbSqlite struct {
@@ -73,7 +74,7 @@ func (db DbSqlite) delete(ctx context.Context, userId uuid.UUID, id uuid.UUID) e
func (db DbSqlite) get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Tag, error) { func (db DbSqlite) get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Tag, error) {
var tag Tag var tag Tag
err := db.db.Get(&tag, "SELECT * FROM tag WHERE id = ? AND user_id = ?", id, userId) err := db.db.GetContext(ctx, &tag, "SELECT * FROM tag WHERE id = ? AND user_id = ?", id, userId)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "Could not get tag", "err", err) slog.ErrorContext(ctx, "Could not get tag", "err", err)
@@ -85,7 +86,7 @@ func (db DbSqlite) get(ctx context.Context, userId uuid.UUID, id uuid.UUID) (*Ta
func (db DbSqlite) getAll(ctx context.Context, userId uuid.UUID) ([]Tag, error) { func (db DbSqlite) getAll(ctx context.Context, userId uuid.UUID) ([]Tag, error) {
var tags []Tag var tags []Tag
err := db.db.Select(&tags, "SELECT * FROM tag WHERE user_id = ?", userId) err := db.db.SelectContext(ctx, &tags, "SELECT * FROM tag WHERE user_id = ?", userId)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "Could not GetAll tag", "err", err) slog.ErrorContext(ctx, "Could not GetAll tag", "err", err)
@@ -94,3 +95,17 @@ func (db DbSqlite) getAll(ctx context.Context, userId uuid.UUID) ([]Tag, error)
return tags, nil return tags, nil
} }
func (db DbSqlite) find(ctx context.Context, userId uuid.UUID, search string) ([]Tag, error) {
var tags []Tag
err := db.db.SelectContext(ctx, &tags, "SELECT * FROM tag WHERE user_id = ? AND name LIKE ?", userId, "%"+search+"%")
slog.InfoContext(ctx, "find", "len", len(tags), "search", search)
if err != nil {
slog.ErrorContext(ctx, "Could not find tag", "err", err)
return nil, core.ErrInternal
}
return tags, nil
}

View File

@@ -30,6 +30,7 @@ func NewHandler(s Service, r *core.Render) Handler {
func (h HandlerImpl) Handle(r *http.ServeMux) { func (h HandlerImpl) Handle(r *http.ServeMux) {
r.Handle("GET /tag", h.handlePage()) r.Handle("GET /tag", h.handlePage())
r.Handle("POST /tag/search", h.handleInlineEditSearch())
r.Handle("GET /tag/new", h.handleNew()) r.Handle("GET /tag/new", h.handleNew())
r.Handle("GET /tag/{id}", h.handleEdit()) r.Handle("GET /tag/{id}", h.handleEdit())
r.Handle("POST /tag/{id}", h.handlePost()) r.Handle("POST /tag/{id}", h.handlePost())
@@ -56,6 +57,26 @@ func (h HandlerImpl) handlePage() http.HandlerFunc {
h.r.RenderLayout(r, w, comp, user) h.r.RenderLayout(r, w, comp, user)
} }
} }
func (h HandlerImpl) handleInlineEditSearch() 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
}
search := r.FormValue("search")
tags, err := h.s.find(r.Context(), user, search)
if err != nil {
core.HandleError(w, r, fmt.Errorf("Could not find tags: %w", core.ErrInternal))
}
comp := inlineEditSearch(tags)
h.r.Render(r, w, comp)
}
}
func (h HandlerImpl) handleNew() http.HandlerFunc { func (h HandlerImpl) handleNew() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {

View File

@@ -14,6 +14,7 @@ type Service interface {
delete(ctx context.Context, user *auth_types.User, tagId uuid.UUID) 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) get(ctx context.Context, user *auth_types.User, tagId uuid.UUID) (*Tag, error)
getAll(ctx context.Context, user *auth_types.User) ([]Tag, error) getAll(ctx context.Context, user *auth_types.User) ([]Tag, error)
find(ctx context.Context, user *auth_types.User, search string) ([]Tag, error)
} }
type ServiceImpl struct { type ServiceImpl struct {
@@ -105,6 +106,13 @@ func (s ServiceImpl) getAll(ctx context.Context, user *auth_types.User) ([]Tag,
return s.db.getAll(ctx, user.Id) return s.db.getAll(ctx, user.Id)
} }
func (s ServiceImpl) find(ctx context.Context, user *auth_types.User, search string) ([]Tag, error) {
if user == nil {
return nil, core.ErrUnauthorized
}
return s.db.find(ctx, user.Id, search)
}
func (s ServiceImpl) isTagValid(tag Tag) bool { func (s ServiceImpl) isTagValid(tag Tag) bool {
err := core.ValidateString(tag.Name, "name") err := core.ValidateString(tag.Name, "name")
return err == nil return err == nil

View File

@@ -5,6 +5,46 @@ import (
"spend-sparrow/internal/template/svg" "spend-sparrow/internal/template/svg"
) )
templ InlineEditInput(classes string) {
<div class={ "flex flex-wrap gap-2 input max-w-full " + classes }>
<span class="flex items-center gap-1 p-1 bg-green-100 rounded-sm">
Lebensmittel
<button class="hover:bg-red-900 rounded p-1 w-5 transition-all">
@svg.Cancel()
</button>
</span>
<span class="flex items-center gap-1 p-1 bg-yellow-100 rounded-sm">
Sparen
<button class="hover:bg-red-900 rounded p-1 w-5 transition-all">
@svg.Cancel()
</button>
</span>
<span class="flex items-center gap-1 p-1 bg-red-100 rounded-sm">
Tanken
<button class="hover:bg-red-900 rounded p-1 w-5 transition-all">
@svg.Cancel()
</button>
</span>
<div>
<input
class="inline"
name="search"
placeholder="Begin Typing To Search ..."
hx-post="/tag/search"
hx-trigger="input changed delay:250ms, keyup[key=='Enter'], load"
hx-target="#tag-search-results"
/>
<div id="tag-search-results" class="absolute bg-white border-gray-200 border-1 rounded-lg p-4"></div>
</div>
</div>
}
templ inlineEditSearch(tags []Tag) {
for _,tag:=range(tags) {
<p x-data={ tag.Id.String() }>{ tag.Name }</p>
}
}
templ page(tags []Tag) { templ page(tags []Tag) {
@core.Breadcrumb([]string{"Home", "Tag"}, []string{"/", "/tag"}) @core.Breadcrumb([]string{"Home", "Tag"}, []string{"/", "/tag"})
<div class="flex flex-wrap gap-20 text-xl mt-10 justify-center"> <div class="flex flex-wrap gap-20 text-xl mt-10 justify-center">