From 800a3298d9a73d1b4255c87987e5257f77db1ce0 Mon Sep 17 00:00:00 2001 From: juancwu Date: Sat, 7 Feb 2026 19:35:06 +0000 Subject: [PATCH 1/3] feat: add expense from overview page --- internal/handler/space.go | 21 +++++++++- internal/ui/components/expense/expense.templ | 41 +++++++++++++++----- internal/ui/pages/app_space_expenses.templ | 7 +++- internal/ui/pages/app_space_overview.templ | 41 +++++++------------- 4 files changed, 71 insertions(+), 39 deletions(-) diff --git a/internal/handler/space.go b/internal/handler/space.go index 254f580..88f9014 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -114,7 +114,14 @@ func (h *SpaceHandler) DashboardPage(w http.ResponseWriter, r *http.Request) { return } - ui.Render(w, r, pages.SpaceOverviewPage(space, lists, tags)) + listsWithItems, err := h.listService.GetListsWithUncheckedItems(spaceID) + if err != nil { + slog.Error("failed to get lists with unchecked items", "error", err, "space_id", spaceID) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + ui.Render(w, r, pages.SpaceOverviewPage(space, lists, tags, listsWithItems)) } func (h *SpaceHandler) ListsPage(w http.ResponseWriter, r *http.Request) { @@ -558,6 +565,18 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) { slog.Error("failed to get balance", "error", err, "space_id", spaceID) } + if r.URL.Query().Get("from") == "overview" { + ui.Render(w, r, toast.Toast(toast.Props{ + Title: "Expense created", + Description: "Your transaction has been recorded.", + Variant: toast.VariantSuccess, + Icon: true, + Dismissible: true, + Duration: 5000, + })) + return + } + ui.Render(w, r, pages.ExpenseCreatedResponse(newExpense, balance)) } diff --git a/internal/ui/components/expense/expense.templ b/internal/ui/components/expense/expense.templ index 0cf2e7e..d3e5200 100644 --- a/internal/ui/components/expense/expense.templ +++ b/internal/ui/components/expense/expense.templ @@ -15,13 +15,36 @@ import ( "git.juancwu.dev/juancwu/budgit/internal/ui/components/tagsinput" ) -templ AddExpenseForm(space *model.Space, tags []*model.Tag, listsWithItems []model.ListWithUncheckedItems) { +type AddExpenseFormProps struct { + Space *model.Space + Tags []*model.Tag + ListsWithItems []model.ListWithUncheckedItems + DialogID string // which dialog to close on success + FromOverview bool // if true, POSTs with ?from=overview for toast response +} + +func (p AddExpenseFormProps) formAttrs() templ.Attributes { + closeScript := "on htmx:afterOnLoad if event.detail.xhr.status == 200 then call window.tui.dialog.close('" + p.DialogID + "') then reset() me end" + if p.FromOverview { + return templ.Attributes{ + "hx-post": "/app/spaces/" + p.Space.ID + "/expenses?from=overview", + "hx-target": "body", + "hx-swap": "beforeend", + "_": closeScript, + } + } + return templ.Attributes{ + "hx-post": "/app/spaces/" + p.Space.ID + "/expenses", + "hx-target": "#expenses-list", + "hx-swap": "afterbegin", + "_": closeScript, + } +} + +templ AddExpenseForm(props AddExpenseFormProps) {
@csrf.Token() // Type @@ -108,7 +131,7 @@ templ AddExpenseForm(space *model.Space, tags []*model.Tag, listsWithItems []mod Tags } - for _, tag := range tags { + for _, tag := range props.Tags { } @@ -124,11 +147,11 @@ templ AddExpenseForm(space *model.Space, tags []*model.Tag, listsWithItems []mod @label.Label(label.Props{}) { Link Shopping List Items } - if len(listsWithItems) == 0 { + if len(props.ListsWithItems) == 0 {

No unchecked items available.

} else {
- for i, lwi := range listsWithItems { + for i, lwi := range props.ListsWithItems { {{ toggleID := "toggle-list-" + lwi.List.ID }} {{ itemsID := "items-" + lwi.List.ID }}
@@ -167,7 +190,7 @@ templ AddExpenseForm(space *model.Space, tags []*model.Tag, listsWithItems []mod }
- if i < len(listsWithItems) - 1 { + if i < len(props.ListsWithItems) - 1 {
} } diff --git a/internal/ui/pages/app_space_expenses.templ b/internal/ui/pages/app_space_expenses.templ index 7a0327c..bfbb0f5 100644 --- a/internal/ui/pages/app_space_expenses.templ +++ b/internal/ui/pages/app_space_expenses.templ @@ -29,7 +29,12 @@ templ SpaceExpensesPage(space *model.Space, expenses []*model.Expense, balance i Add a new expense or top-up to your space. } } - @expense.AddExpenseForm(space, tags, listsWithItems) + @expense.AddExpenseForm(expense.AddExpenseFormProps{ + Space: space, + Tags: tags, + ListsWithItems: listsWithItems, + DialogID: "add-expense-dialog", + }) } } diff --git a/internal/ui/pages/app_space_overview.templ b/internal/ui/pages/app_space_overview.templ index 38bff40..ea41605 100644 --- a/internal/ui/pages/app_space_overview.templ +++ b/internal/ui/pages/app_space_overview.templ @@ -3,53 +3,38 @@ package pages import ( "git.juancwu.dev/juancwu/budgit/internal/model" "git.juancwu.dev/juancwu/budgit/internal/ui/components/button" - "git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf" "git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog" - "git.juancwu.dev/juancwu/budgit/internal/ui/components/input" + "git.juancwu.dev/juancwu/budgit/internal/ui/components/expense" "git.juancwu.dev/juancwu/budgit/internal/ui/layouts" ) -templ SpaceOverviewPage(space *model.Space, lists []*model.ShoppingList, tags []*model.Tag) { +templ SpaceOverviewPage(space *model.Space, lists []*model.ShoppingList, tags []*model.Tag, listsWithItems []model.ListWithUncheckedItems) { @layouts.Space("Overview", space) {

Welcome to { space.Name }!

- @dialog.Dialog(dialog.Props{ID: "invite-member-dialog"}) { + @dialog.Dialog(dialog.Props{ID: "add-expense-overview-dialog"}) { @dialog.Trigger() { @button.Button() { - Invite Member + Add Expense } } @dialog.Content() { @dialog.Header() { @dialog.Title() { - Invite Member + Add Transaction } @dialog.Description() { - Send an invitation email to add a new member to this space. + Add a new expense or top-up to your space. } } - - @csrf.Token() -
- - @input.Input(input.Props{ - Name: "email", - ID: "email", - Type: "email", - Attributes: templ.Attributes{"required": "true"}, - }) -
-
- @button.Button(button.Props{Type: button.TypeSubmit}) { - Send Invitation - } -
- + @expense.AddExpenseForm(expense.AddExpenseFormProps{ + Space: space, + Tags: tags, + ListsWithItems: listsWithItems, + DialogID: "add-expense-overview-dialog", + FromOverview: true, + }) } }
-- 2.43.0 From ccd6eaf9ee64019e904cdf46491999fe991841ed Mon Sep 17 00:00:00 2001 From: juancwu Date: Sat, 7 Feb 2026 19:37:05 +0000 Subject: [PATCH 2/3] feat: redirect to expenses after add from overview --- internal/handler/space.go | 10 ++-------- internal/ui/components/expense/expense.templ | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/internal/handler/space.go b/internal/handler/space.go index 88f9014..fbd3c7c 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -566,14 +566,8 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) { } if r.URL.Query().Get("from") == "overview" { - ui.Render(w, r, toast.Toast(toast.Props{ - Title: "Expense created", - Description: "Your transaction has been recorded.", - Variant: toast.VariantSuccess, - Icon: true, - Dismissible: true, - Duration: 5000, - })) + w.Header().Set("HX-Redirect", "/app/spaces/"+spaceID+"/expenses") + w.WriteHeader(http.StatusOK) return } diff --git a/internal/ui/components/expense/expense.templ b/internal/ui/components/expense/expense.templ index d3e5200..2f521f8 100644 --- a/internal/ui/components/expense/expense.templ +++ b/internal/ui/components/expense/expense.templ @@ -20,7 +20,7 @@ type AddExpenseFormProps struct { Tags []*model.Tag ListsWithItems []model.ListWithUncheckedItems DialogID string // which dialog to close on success - FromOverview bool // if true, POSTs with ?from=overview for toast response + FromOverview bool // if true, POSTs with ?from=overview; server redirects to expenses page } func (p AddExpenseFormProps) formAttrs() templ.Attributes { -- 2.43.0 From cb3edae1e9ab6e597d4981b21cf68ad156d3cf87 Mon Sep 17 00:00:00 2001 From: juancwu Date: Sat, 7 Feb 2026 19:40:59 +0000 Subject: [PATCH 3/3] feat: render success toast on redirect after adding expense --- internal/handler/space.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/handler/space.go b/internal/handler/space.go index fbd3c7c..030feea 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -437,6 +437,17 @@ func (h *SpaceHandler) ExpensesPage(w http.ResponseWriter, r *http.Request) { } ui.Render(w, r, pages.SpaceExpensesPage(space, expenses, balance, tags, listsWithItems)) + + if r.URL.Query().Get("created") == "true" { + ui.Render(w, r, toast.Toast(toast.Props{ + Title: "Expense created", + Description: "Your transaction has been recorded.", + Variant: toast.VariantSuccess, + Icon: true, + Dismissible: true, + Duration: 5000, + })) + } } func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) { @@ -566,7 +577,7 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) { } if r.URL.Query().Get("from") == "overview" { - w.Header().Set("HX-Redirect", "/app/spaces/"+spaceID+"/expenses") + w.Header().Set("HX-Redirect", "/app/spaces/"+spaceID+"/expenses?created=true") w.WriteHeader(http.StatusOK) return } -- 2.43.0