From f10044eac1997537bcdf7699f5b4284aac16f8e2 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 1 Feb 2026 14:47:50 -1000 Subject: Improve shopping mode and flatten nav bar Shopping mode: - Click to complete items (deletes user items, hides external items) - Add print button with compact two-column print layout - Fix CSRF token for HTMX requests - Fix input clearing with proper htmx:afterRequest handler - Remove "Quick Add" store option, require valid store Navigation: - Replace dropdown menu with flat nav showing all tabs - Remove unused dropdown JS Tests: - Add TestHandleShoppingModeComplete for user and external items Co-Authored-By: Claude Opus 4.5 --- internal/handlers/handlers.go | 65 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) (limited to 'internal/handlers/handlers.go') diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index c384c48..f0f2a19 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1258,7 +1258,8 @@ func (h *Handler) HandleShoppingQuickAdd(w http.ResponseWriter, r *http.Request) return } if storeName == "" { - storeName = "Quick Add" + JSONError(w, http.StatusBadRequest, "Store is required", nil) + return } if err := h.store.SaveUserShoppingItem(name, storeName); err != nil { @@ -1380,10 +1381,12 @@ func (h *Handler) HandleShoppingMode(w http.ResponseWriter, r *http.Request) { StoreName string Items []models.UnifiedShoppingItem StoreNames []string + CSRFToken string }{ StoreName: storeName, Items: items, StoreNames: storeNames, + CSRFToken: auth.GetCSRFTokenFromContext(ctx), } HTMLResponse(w, h.templates, "shopping-mode.html", data) @@ -1450,6 +1453,64 @@ func (h *Handler) HandleShoppingModeToggle(w http.ResponseWriter, r *http.Reques }{storeName, items}) } +// HandleShoppingModeComplete removes an item from the shopping list +func (h *Handler) HandleShoppingModeComplete(w http.ResponseWriter, r *http.Request) { + storeName := chi.URLParam(r, "store") + if err := r.ParseForm(); err != nil { + JSONError(w, http.StatusBadRequest, "Failed to parse form", err) + return + } + + id := r.FormValue("id") + source := r.FormValue("source") + + // Complete (remove) the item + switch source { + case "user": + var userID int64 + if _, err := fmt.Sscanf(id, "user-%d", &userID); err != nil { + JSONError(w, http.StatusBadRequest, "Invalid user item ID", err) + return + } + if err := h.store.DeleteUserShoppingItem(userID); err != nil { + JSONError(w, http.StatusInternalServerError, "Failed to delete item", err) + return + } + case "trello", "plantoeat": + // Mark as checked (will be filtered out of view) + if err := h.store.SetShoppingItemChecked(source, id, true); err != nil { + JSONError(w, http.StatusInternalServerError, "Failed to complete item", err) + return + } + } + + // URL decode the store name + storeName, _ = url.QueryUnescape(storeName) + + // Return updated item list partial + ctx := r.Context() + allStores := h.aggregateShoppingLists(ctx) + + var items []models.UnifiedShoppingItem + for _, store := range allStores { + if strings.EqualFold(store.Name, storeName) { + for _, cat := range store.Categories { + items = append(items, cat.Items...) + } + } + } + + // Sort alphabetically (checked items already filtered in template) + sort.Slice(items, func(i, j int) bool { + return items[i].Name < items[j].Name + }) + + HTMLResponse(w, h.templates, "shopping-mode-items", struct { + StoreName string + Items []models.UnifiedShoppingItem + }{storeName, items}) +} + // aggregateShoppingLists combines Trello, PlanToEat, and user shopping items by store func (h *Handler) aggregateShoppingLists(ctx context.Context) []models.ShoppingStore { storeMap := make(map[string]map[string][]models.UnifiedShoppingItem) @@ -1535,7 +1596,7 @@ func (h *Handler) aggregateShoppingLists(ctx context.Context) []models.ShoppingS for _, item := range userItems { storeName := item.Store if storeName == "" { - storeName = "Quick Add" + continue // Skip items without a store } if storeMap[storeName] == nil { -- cgit v1.2.3