Merge pull request 'Add expense from overview page' (#3) from feat/add-expense-shortcut into main

Reviewed-on: #3
This commit is contained in:
juancwu 2026-02-07 19:41:27 +00:00
commit cd2eac6b88
4 changed files with 76 additions and 39 deletions

View file

@ -114,7 +114,14 @@ func (h *SpaceHandler) DashboardPage(w http.ResponseWriter, r *http.Request) {
return 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) { func (h *SpaceHandler) ListsPage(w http.ResponseWriter, r *http.Request) {
@ -430,6 +437,17 @@ func (h *SpaceHandler) ExpensesPage(w http.ResponseWriter, r *http.Request) {
} }
ui.Render(w, r, pages.SpaceExpensesPage(space, expenses, balance, tags, listsWithItems)) 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) { func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) {
@ -558,6 +576,12 @@ func (h *SpaceHandler) CreateExpense(w http.ResponseWriter, r *http.Request) {
slog.Error("failed to get balance", "error", err, "space_id", spaceID) slog.Error("failed to get balance", "error", err, "space_id", spaceID)
} }
if r.URL.Query().Get("from") == "overview" {
w.Header().Set("HX-Redirect", "/app/spaces/"+spaceID+"/expenses?created=true")
w.WriteHeader(http.StatusOK)
return
}
ui.Render(w, r, pages.ExpenseCreatedResponse(newExpense, balance)) ui.Render(w, r, pages.ExpenseCreatedResponse(newExpense, balance))
} }

View file

@ -15,13 +15,36 @@ import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/tagsinput" "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; server redirects to expenses page
}
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) {
<form <form
hx-post={ "/app/spaces/" + space.ID + "/expenses" }
hx-target="#expenses-list"
hx-swap="afterbegin"
_="on htmx:afterOnLoad if event.detail.xhr.status == 200 then call window.tui.dialog.close('add-expense-dialog') reset() me end"
class="space-y-4" class="space-y-4"
{ props.formAttrs()... }
> >
@csrf.Token() @csrf.Token()
// Type // Type
@ -108,7 +131,7 @@ templ AddExpenseForm(space *model.Space, tags []*model.Tag, listsWithItems []mod
Tags Tags
} }
<datalist id="available-tags"> <datalist id="available-tags">
for _, tag := range tags { for _, tag := range props.Tags {
<option value={ tag.Name }></option> <option value={ tag.Name }></option>
} }
</datalist> </datalist>
@ -124,11 +147,11 @@ templ AddExpenseForm(space *model.Space, tags []*model.Tag, listsWithItems []mod
@label.Label(label.Props{}) { @label.Label(label.Props{}) {
Link Shopping List Items Link Shopping List Items
} }
if len(listsWithItems) == 0 { if len(props.ListsWithItems) == 0 {
<p class="text-sm text-muted-foreground">No unchecked items available.</p> <p class="text-sm text-muted-foreground">No unchecked items available.</p>
} else { } else {
<div class="max-h-48 overflow-y-auto border rounded-md p-2 space-y-2"> <div class="max-h-48 overflow-y-auto border rounded-md p-2 space-y-2">
for i, lwi := range listsWithItems { for i, lwi := range props.ListsWithItems {
{{ toggleID := "toggle-list-" + lwi.List.ID }} {{ toggleID := "toggle-list-" + lwi.List.ID }}
{{ itemsID := "items-" + lwi.List.ID }} {{ itemsID := "items-" + lwi.List.ID }}
<div class="space-y-1"> <div class="space-y-1">
@ -167,7 +190,7 @@ templ AddExpenseForm(space *model.Space, tags []*model.Tag, listsWithItems []mod
} }
</div> </div>
</div> </div>
if i < len(listsWithItems) - 1 { if i < len(props.ListsWithItems) - 1 {
<hr class="border-border"/> <hr class="border-border"/>
} }
} }

View file

@ -29,7 +29,12 @@ templ SpaceExpensesPage(space *model.Space, expenses []*model.Expense, balance i
Add a new expense or top-up to your space. 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",
})
} }
} }
</div> </div>

View file

@ -3,53 +3,38 @@ package pages
import ( import (
"git.juancwu.dev/juancwu/budgit/internal/model" "git.juancwu.dev/juancwu/budgit/internal/model"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button" "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/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" "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) { @layouts.Space("Overview", space) {
<div class="space-y-4"> <div class="space-y-4">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h1 class="text-2xl font-bold">Welcome to { space.Name }!</h1> <h1 class="text-2xl font-bold">Welcome to { space.Name }!</h1>
@dialog.Dialog(dialog.Props{ID: "invite-member-dialog"}) { @dialog.Dialog(dialog.Props{ID: "add-expense-overview-dialog"}) {
@dialog.Trigger() { @dialog.Trigger() {
@button.Button() { @button.Button() {
Invite Member Add Expense
} }
} }
@dialog.Content() { @dialog.Content() {
@dialog.Header() { @dialog.Header() {
@dialog.Title() { @dialog.Title() {
Invite Member Add Transaction
} }
@dialog.Description() { @dialog.Description() {
Send an invitation email to add a new member to this space. Add a new expense or top-up to your space.
} }
} }
<form @expense.AddExpenseForm(expense.AddExpenseFormProps{
hx-post={ "/app/spaces/" + space.ID + "/invites" } Space: space,
hx-swap="innerHTML" Tags: tags,
class="space-y-4" ListsWithItems: listsWithItems,
> DialogID: "add-expense-overview-dialog",
@csrf.Token() FromOverview: true,
<div> })
<label for="email" class="label">Email Address</label>
@input.Input(input.Props{
Name: "email",
ID: "email",
Type: "email",
Attributes: templ.Attributes{"required": "true"},
})
</div>
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
Send Invitation
}
</div>
</form>
} }
} }
</div> </div>