From 6c704828cecd1e586d340f456ecc4fba9f39c709 Mon Sep 17 00:00:00 2001 From: juancwu Date: Sat, 7 Feb 2026 18:27:06 +0000 Subject: [PATCH] feat: show shopping list items in cards --- assets/js/htmx-csrf.js | 11 + assets/js/smooth-scroll.js | 18 ++ assets/js/theme.js | 13 ++ go.mod | 3 +- go.sum | 4 + internal/handler/space.go | 76 +++++- internal/model/shopping_list.go | 7 + internal/repository/list_item.go | 19 ++ internal/routes/routes.go | 4 + internal/service/shopping_list.go | 28 +++ .../shoppinglist/shoppinglist.templ | 221 +++++++++++++++++- internal/ui/layouts/base.templ | 53 +---- internal/ui/pages/app_space_lists.templ | 10 +- internal/ui/pages/app_space_overview.templ | 14 +- 14 files changed, 396 insertions(+), 85 deletions(-) create mode 100644 assets/js/htmx-csrf.js create mode 100644 assets/js/smooth-scroll.js create mode 100644 assets/js/theme.js diff --git a/assets/js/htmx-csrf.js b/assets/js/htmx-csrf.js new file mode 100644 index 0000000..ce8d840 --- /dev/null +++ b/assets/js/htmx-csrf.js @@ -0,0 +1,11 @@ +document.addEventListener('DOMContentLoaded', function() { + // Listen for htmx requests and add CSRF token header + document.body.addEventListener('htmx:configRequest', function(event) { + // Get CSRF token from meta tag + const meta = document.querySelector('meta[name="csrf-token"]'); + if (meta) { + // Add token as X-CSRF-Token header to all HTMX requests + event.detail.headers['X-CSRF-Token'] = meta.getAttribute('content'); + } + }); +}); diff --git a/assets/js/smooth-scroll.js b/assets/js/smooth-scroll.js new file mode 100644 index 0000000..24b1d1f --- /dev/null +++ b/assets/js/smooth-scroll.js @@ -0,0 +1,18 @@ +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + // Only prevent default for same-page anchors + const href = this.getAttribute('href'); + if (href && href !== '#' && href.startsWith('#')) { + const target = document.querySelector(href); + if (target) { + e.preventDefault(); + target.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + } + }); + }); +}); diff --git a/assets/js/theme.js b/assets/js/theme.js new file mode 100644 index 0000000..cfce569 --- /dev/null +++ b/assets/js/theme.js @@ -0,0 +1,13 @@ +// Apply saved theme or system preference on load +if (localStorage.theme === 'dark' || (!localStorage.theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark'); +} + +// Theme toggle handler +document.addEventListener('click', (e) => { + if (e.target.closest('[data-theme-switcher]')) { + e.preventDefault(); + const isDark = document.documentElement.classList.toggle('dark'); + localStorage.theme = isDark ? 'dark' : 'light'; + } +}); diff --git a/go.mod b/go.mod index 06e6ba2..b997218 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.25.1 require ( github.com/Oudwins/tailwind-merge-go v0.2.1 - github.com/a-h/templ v0.3.960 github.com/alexedwards/argon2id v1.0.0 github.com/emersion/go-imap v1.2.1 github.com/golang-jwt/jwt/v5 v5.2.2 @@ -22,6 +21,7 @@ require ( github.com/ClickHouse/ch-go v0.67.0 // indirect github.com/ClickHouse/clickhouse-go/v2 v2.40.1 // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect + github.com/a-h/templ v0.3.977 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -59,7 +59,6 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/templui/templui v0.101.0 // indirect github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect github.com/vertica/vertica-sql-go v1.3.3 // indirect github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 // indirect diff --git a/go.sum b/go.sum index 1faecc3..92925f2 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ6 github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM= github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= +github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= @@ -48,6 +50,7 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -184,6 +187,7 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/handler/space.go b/internal/handler/space.go index 12261fd..a825b73 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -125,14 +125,14 @@ func (h *SpaceHandler) ListsPage(w http.ResponseWriter, r *http.Request) { return } - lists, err := h.listService.GetListsForSpace(spaceID) + cards, err := h.buildListCards(spaceID) if err != nil { - slog.Error("failed to get lists for space", "error", err, "space_id", spaceID) + slog.Error("failed to build list cards", "error", err, "space_id", spaceID) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - ui.Render(w, r, pages.SpaceListsPage(space, lists)) + ui.Render(w, r, pages.SpaceListsPage(space, cards)) } func (h *SpaceHandler) CreateList(w http.ResponseWriter, r *http.Request) { @@ -158,7 +158,7 @@ func (h *SpaceHandler) CreateList(w http.ResponseWriter, r *http.Request) { return } - ui.Render(w, r, shoppinglist.ListItem(newList)) + ui.Render(w, r, shoppinglist.ListCard(spaceID, newList, nil, 1, 1)) } func (h *SpaceHandler) UpdateList(w http.ResponseWriter, r *http.Request) { @@ -187,7 +187,11 @@ func (h *SpaceHandler) UpdateList(w http.ResponseWriter, r *http.Request) { return } - ui.Render(w, r, shoppinglist.ListNameHeader(spaceID, updatedList)) + if r.URL.Query().Get("from") == "card" { + ui.Render(w, r, shoppinglist.ListCardHeader(spaceID, updatedList)) + } else { + ui.Render(w, r, shoppinglist.ListNameHeader(spaceID, updatedList)) + } } func (h *SpaceHandler) DeleteList(w http.ResponseWriter, r *http.Request) { @@ -205,7 +209,9 @@ func (h *SpaceHandler) DeleteList(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("HX-Redirect", "/app/spaces/"+spaceID+"/lists") + if r.URL.Query().Get("from") != "card" { + w.Header().Set("HX-Redirect", "/app/spaces/"+spaceID+"/lists") + } w.WriteHeader(http.StatusOK) } @@ -293,7 +299,11 @@ func (h *SpaceHandler) ToggleItem(w http.ResponseWriter, r *http.Request) { return } - ui.Render(w, r, shoppinglist.ItemDetail(spaceID, updatedItem)) + if r.URL.Query().Get("from") == "card" { + ui.Render(w, r, shoppinglist.CardItemDetail(spaceID, updatedItem)) + } else { + ui.Render(w, r, shoppinglist.ItemDetail(spaceID, updatedItem)) + } } func (h *SpaceHandler) DeleteItem(w http.ResponseWriter, r *http.Request) { @@ -636,12 +646,58 @@ func (h *SpaceHandler) GetShoppingListItems(w http.ResponseWriter, r *http.Reque func (h *SpaceHandler) GetLists(w http.ResponseWriter, r *http.Request) { spaceID := r.PathValue("spaceID") - lists, err := h.listService.GetListsForSpace(spaceID) + cards, err := h.buildListCards(spaceID) if err != nil { - slog.Error("failed to get lists", "error", err, "space_id", spaceID) + slog.Error("failed to build list cards", "error", err, "space_id", spaceID) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - ui.Render(w, r, pages.ListsContainer(lists)) + ui.Render(w, r, pages.ListsContainer(spaceID, cards)) +} + +func (h *SpaceHandler) GetListCardItems(w http.ResponseWriter, r *http.Request) { + spaceID := r.PathValue("spaceID") + listID := r.PathValue("listID") + + if h.getListForSpace(w, spaceID, listID) == nil { + return + } + + page := 1 + if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 0 { + page = p + } + + items, totalPages, err := h.listService.GetItemsForListPaginated(listID, page) + if err != nil { + slog.Error("failed to get paginated items", "error", err, "list_id", listID) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + ui.Render(w, r, shoppinglist.ListCardItems(spaceID, listID, items, page, totalPages)) +} + +func (h *SpaceHandler) buildListCards(spaceID string) ([]model.ListCardData, error) { + lists, err := h.listService.GetListsForSpace(spaceID) + if err != nil { + return nil, err + } + + cards := make([]model.ListCardData, len(lists)) + for i, list := range lists { + items, totalPages, err := h.listService.GetItemsForListPaginated(list.ID, 1) + if err != nil { + return nil, err + } + cards[i] = model.ListCardData{ + List: list, + Items: items, + CurrentPage: 1, + TotalPages: totalPages, + } + } + + return cards, nil } diff --git a/internal/model/shopping_list.go b/internal/model/shopping_list.go index ff0f8d8..c9abf59 100644 --- a/internal/model/shopping_list.go +++ b/internal/model/shopping_list.go @@ -19,3 +19,10 @@ type ListItem struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type ListCardData struct { + List *ShoppingList + Items []*ListItem + CurrentPage int + TotalPages int +} diff --git a/internal/repository/list_item.go b/internal/repository/list_item.go index d8b4a93..b8b39a5 100644 --- a/internal/repository/list_item.go +++ b/internal/repository/list_item.go @@ -17,6 +17,8 @@ type ListItemRepository interface { Create(item *model.ListItem) error GetByID(id string) (*model.ListItem, error) GetByListID(listID string) ([]*model.ListItem, error) + GetByListIDPaginated(listID string, limit, offset int) ([]*model.ListItem, error) + CountByListID(listID string) (int, error) Update(item *model.ListItem) error Delete(id string) error DeleteByListID(listID string) error @@ -56,6 +58,23 @@ func (r *listItemRepository) GetByListID(listID string) ([]*model.ListItem, erro return items, nil } +func (r *listItemRepository) GetByListIDPaginated(listID string, limit, offset int) ([]*model.ListItem, error) { + var items []*model.ListItem + query := `SELECT * FROM list_items WHERE list_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3;` + err := r.db.Select(&items, query, listID, limit, offset) + if err != nil { + return nil, err + } + return items, nil +} + +func (r *listItemRepository) CountByListID(listID string) (int, error) { + var count int + query := `SELECT COUNT(*) FROM list_items WHERE list_id = $1;` + err := r.db.Get(&count, query, listID) + return count, err +} + func (r *listItemRepository) Update(item *model.ListItem) error { item.UpdatedAt = time.Now() query := `UPDATE list_items SET name = $1, is_checked = $2, updated_at = $3 WHERE id = $4;` diff --git a/internal/routes/routes.go b/internal/routes/routes.go index bd1735c..2497cf3 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -131,6 +131,10 @@ func SetupRoutes(a *app.App) http.Handler { shoppingListItemsWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(shoppingListItemsHandler) mux.Handle("GET /app/spaces/{spaceID}/lists/{listID}/items", shoppingListItemsWithAccess) + cardItemsHandler := middleware.RequireAuth(space.GetListCardItems) + cardItemsWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(cardItemsHandler) + mux.Handle("GET /app/spaces/{spaceID}/lists/{listID}/card-items", cardItemsWithAccess) + listsComponentHandler := middleware.RequireAuth(space.GetLists) listsComponentWithAccess := middleware.RequireSpaceAccess(a.SpaceService)(listsComponentHandler) mux.Handle("GET /app/spaces/{spaceID}/components/lists", listsComponentWithAccess) diff --git a/internal/service/shopping_list.go b/internal/service/shopping_list.go index d694a53..e0f3633 100644 --- a/internal/service/shopping_list.go +++ b/internal/service/shopping_list.go @@ -140,6 +140,34 @@ func (s *ShoppingListService) GetItemsForList(listID string) ([]*model.ListItem, return s.itemRepo.GetByListID(listID) } +const ItemsPerCardPage = 5 + +func (s *ShoppingListService) GetItemsForListPaginated(listID string, page int) ([]*model.ListItem, int, error) { + total, err := s.itemRepo.CountByListID(listID) + if err != nil { + return nil, 0, err + } + + totalPages := (total + ItemsPerCardPage - 1) / ItemsPerCardPage + if totalPages < 1 { + totalPages = 1 + } + if page < 1 { + page = 1 + } + if page > totalPages { + page = totalPages + } + + offset := (page - 1) * ItemsPerCardPage + items, err := s.itemRepo.GetByListIDPaginated(listID, ItemsPerCardPage, offset) + if err != nil { + return nil, 0, err + } + + return items, totalPages, nil +} + func (s *ShoppingListService) UpdateItem(itemID, name string, isChecked bool) (*model.ListItem, error) { name = strings.TrimSpace(name) if name == "" { diff --git a/internal/ui/components/shoppinglist/shoppinglist.templ b/internal/ui/components/shoppinglist/shoppinglist.templ index a2843b9..2f0b147 100644 --- a/internal/ui/components/shoppinglist/shoppinglist.templ +++ b/internal/ui/components/shoppinglist/shoppinglist.templ @@ -2,12 +2,221 @@ package shoppinglist import ( "fmt" + "strconv" "git.juancwu.dev/juancwu/budgit/internal/model" + "git.juancwu.dev/juancwu/budgit/internal/ui/components/button" "git.juancwu.dev/juancwu/budgit/internal/ui/components/checkbox" "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/icon" "git.juancwu.dev/juancwu/budgit/internal/ui/components/input" + "git.juancwu.dev/juancwu/budgit/internal/ui/components/pagination" ) +// ListCard renders a full shopping list card with inline items, add form, and pagination. +templ ListCard(spaceID string, list *model.ShoppingList, items []*model.ListItem, currentPage, totalPages int) { +
+
+ @ListCardHeader(spaceID, list) +
+
+
+ @csrf.Token() + @input.Input(input.Props{ + Name: "name", + Placeholder: "Add item...", + Class: "h-8 text-sm", + Attributes: templ.Attributes{ + "autocomplete": "off", + }, + }) + @button.Button(button.Props{ + Type: button.TypeSubmit, + Size: button.SizeSm, + }) { + @icon.Plus(icon.Props{Size: 16}) + } +
+
+
+ @ListCardItems(spaceID, list.ID, items, currentPage, totalPages) +
+
+} + +// ListCardHeader renders the card header with name display, edit form, and delete button. +templ ListCardHeader(spaceID string, list *model.ShoppingList) { +
+

{ list.Name }

+
+ + @dialog.Dialog(dialog.Props{ID: "del-list-" + list.ID}) { + @dialog.Trigger() { + + } + @dialog.Content() { + @dialog.Header() { + @dialog.Title() { + Delete Shopping List + } + @dialog.Description() { + Are you sure you want to delete "{ list.Name }"? This will permanently remove the list and all its items. + } + } + @dialog.Footer() { + @dialog.Close() { + @button.Button(button.Props{Variant: button.VariantOutline}) { + Cancel + } + } + @button.Button(button.Props{ + Variant: button.VariantDestructive, + Attributes: templ.Attributes{ + "hx-delete": fmt.Sprintf("/app/spaces/%s/lists/%s?from=card", spaceID, list.ID), + "hx-target": "#list-card-" + list.ID, + "hx-swap": "outerHTML", + }, + }) { + Delete + } + } + } + } +
+
+ +} + +// ListCardItems renders the paginated items section within a card. +templ ListCardItems(spaceID string, listID string, items []*model.ListItem, currentPage, totalPages int) { + if len(items) == 0 { +

No items yet

+ } else { +
+ for _, item := range items { + @CardItemDetail(spaceID, item) + } +
+ } + if totalPages > 1 { +
+ @pagination.Pagination(pagination.Props{Class: "justify-center"}) { + @pagination.Content() { + @pagination.Item() { + @pagination.Previous(pagination.PreviousProps{ + Disabled: currentPage <= 1, + Attributes: templ.Attributes{ + "hx-get": fmt.Sprintf("/app/spaces/%s/lists/%s/card-items?page=%d", spaceID, listID, currentPage-1), + "hx-target": "#list-items-" + listID, + "hx-swap": "innerHTML", + }, + }) + } + for _, pg := range pagination.CreatePagination(currentPage, totalPages, 3).Pages { + @pagination.Item() { + @pagination.Link(pagination.LinkProps{ + IsActive: pg == currentPage, + Attributes: templ.Attributes{ + "hx-get": fmt.Sprintf("/app/spaces/%s/lists/%s/card-items?page=%d", spaceID, listID, pg), + "hx-target": "#list-items-" + listID, + "hx-swap": "innerHTML", + }, + }) { + { strconv.Itoa(pg) } + } + } + } + @pagination.Item() { + @pagination.Next(pagination.NextProps{ + Disabled: currentPage >= totalPages, + Attributes: templ.Attributes{ + "hx-get": fmt.Sprintf("/app/spaces/%s/lists/%s/card-items?page=%d", spaceID, listID, currentPage+1), + "hx-target": "#list-items-" + listID, + "hx-swap": "innerHTML", + }, + }) + } + } + } +
+ } +} + +// CardItemDetail renders an item within a card. Toggle is in-place, delete triggers a refresh. +templ CardItemDetail(spaceID string, item *model.ListItem) { +
+ @checkbox.Checkbox(checkbox.Props{ + ID: "item-" + item.ID + "-checkbox", + Name: "is_checked", + Checked: item.IsChecked, + Attributes: templ.Attributes{ + "hx-patch": fmt.Sprintf("/app/spaces/%s/lists/%s/items/%s?from=card", spaceID, item.ListID, item.ID), + "hx-target": "#item-" + item.ID, + "hx-swap": "outerHTML", + }, + }) + { item.Name } + +
+} + +// ListNameHeader is used on the detail page for editing list name inline. templ ListNameHeader(spaceID string, list *model.ShoppingList) {

{ list.Name }

@@ -15,7 +224,7 @@ templ ListNameHeader(spaceID string, list *model.ShoppingList) { class="text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100 transition-opacity" _="on click toggle .hidden on #list-name-header then toggle .hidden on #list-name-edit then focus() the first in #list-name-edit" > - + @icon.Pencil(icon.Props{Size: 16})
} -templ ListItem(list *model.ShoppingList) { - -
- { list.Name } - // TODO: Add item count or other info -
-
-} - +// ItemDetail renders an individual item row (used by the detail page and toggle responses). templ ItemDetail(spaceID string, item *model.ListItem) {
@checkbox.Checkbox(checkbox.Props{ diff --git a/internal/ui/layouts/base.templ b/internal/ui/layouts/base.templ index 64f7caa..b95a1d3 100644 --- a/internal/ui/layouts/base.templ +++ b/internal/ui/layouts/base.templ @@ -103,60 +103,13 @@ templ seo(props SEOProps) { } templ themeScript() { - + } templ smoothScrollScript() { - // Smooth scrolling for anchor links - works site-wide - + } templ htmxCSRFScript() { - // Configure HTMX to automatically send CSRF token with all requests - + } diff --git a/internal/ui/pages/app_space_lists.templ b/internal/ui/pages/app_space_lists.templ index b1b7b3a..f1c8483 100644 --- a/internal/ui/pages/app_space_lists.templ +++ b/internal/ui/pages/app_space_lists.templ @@ -9,7 +9,7 @@ import ( "git.juancwu.dev/juancwu/budgit/internal/ui/layouts" ) -templ SpaceListsPage(space *model.Space, lists []*model.ShoppingList) { +templ SpaceListsPage(space *model.Space, cards []model.ListCardData) { @layouts.Space("Shopping Lists", space) {
@@ -40,14 +40,14 @@ templ SpaceListsPage(space *model.Space, lists []*model.ShoppingList) { hx-trigger="sse:list_created, sse:list_deleted" hx-swap="innerHTML" > - @ListsContainer(lists) + @ListsContainer(space.ID, cards)
} } -templ ListsContainer(lists []*model.ShoppingList) { - for _, list := range lists { - @shoppinglist.ListItem(list) +templ ListsContainer(spaceID string, cards []model.ListCardData) { + for _, card := range cards { + @shoppinglist.ListCard(spaceID, card.List, card.Items, card.CurrentPage, card.TotalPages) } } diff --git a/internal/ui/pages/app_space_overview.templ b/internal/ui/pages/app_space_overview.templ index 267fa2b..38bff40 100644 --- a/internal/ui/pages/app_space_overview.templ +++ b/internal/ui/pages/app_space_overview.templ @@ -72,14 +72,12 @@ templ SpaceOverviewPage(space *model.Space, lists []*model.ShoppingList, tags [] } } - if len(lists) > 5 { - - View all shopping lists - - } + + View all shopping lists + } else {

No shopping lists yet.

}