ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

pure htmx removal (#495)

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
49583bf2 c7c3a54d

+76 -11
+6 -1
internal/templates/user.html
··· 138 138 <a href="/recipe/{{$recipe.Hash}}" class="hover:text-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2">{{$recipe.Title}}</a> 139 139 {{- if $recipe.Cooked}} <span aria-label="{{$recipe.CookedStarsLabel}}" title="{{$recipe.CookedStarsLabel}}">{{$recipe.CookedStars}}</span>{{end}} 140 140 </div> 141 - <form method="POST" action="/user/recipes/remove" class="shrink-0"> 141 + <form method="POST" 142 + hx-post="/user/recipes/remove" 143 + hx-target="closest li" 144 + hx-swap="delete" 145 + class="shrink-0"> 142 146 <input type="hidden" name="hash" value="{{$recipe.Hash}}" /> 143 147 <button type="submit" 144 148 class="inline-flex h-7 w-7 items-center justify-center rounded-full border border-brand-200 text-brand-600 transition hover:bg-brand-50 hover:text-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:ring-offset-2" ··· 162 166 <p class="mt-6 text-center text-sm text-gray-500">Need help? Reach out from the home page.</p> 163 167 </section> 164 168 </main> 169 + <script src="/static/htmx@2.0.8.js"></script> 165 170 {{template "clerk_refresh.html" .}} 166 171 </body> 167 172 </html>
+7 -3
internal/users/server.go
··· 108 108 109 109 func (s *server) handleRemoveUserRecipe(w http.ResponseWriter, r *http.Request) { 110 110 ctx := r.Context() 111 + if !isHTMXRequest(r) { 112 + http.Error(w, "htmx request required", http.StatusBadRequest) 113 + return 114 + } 111 115 112 116 currentUser, err := s.storage.FromRequest(ctx, r, s.clerk) 113 117 if err != nil { ··· 116 120 http.Error(w, "unable to load account", http.StatusInternalServerError) 117 121 return 118 122 } 119 - http.Redirect(w, r, "/", http.StatusSeeOther) 123 + http.Error(w, "no valid session found", http.StatusUnauthorized) 120 124 return 121 125 } 122 126 ··· 129 133 } 130 134 if !removed { 131 135 slog.ErrorContext(ctx, "why did we get a fail to remove?", "hash", recipeHash) 132 - http.Redirect(w, r, "/user?tab=past", http.StatusSeeOther) 136 + http.Error(w, "recipe not found", http.StatusNotFound) 133 137 return 134 138 } 135 139 136 - http.Redirect(w, r, "/user?tab=past", http.StatusSeeOther) 140 + w.WriteHeader(http.StatusOK) 137 141 } 138 142 139 143 func (s *server) handleUser(w http.ResponseWriter, r *http.Request) {
+63 -7
internal/users/server_test.go
··· 226 226 } 227 227 228 228 body := rr.Body.String() 229 + if !strings.Contains(body, `/static/htmx@2.0.8.js`) { 230 + t.Fatalf("expected user page to include htmx script, got body: %s", body) 231 + } 229 232 if !strings.Contains(body, `Cooked Pasta</a> <span aria-label="Rated 4 stars" title="Rated 4 stars">⭐⭐⭐⭐</span>`) { 230 233 t.Fatalf("expected cooked recipe to render 4 stars, got body: %s", body) 231 234 } ··· 244 247 if strings.Contains(body, `Cooked Five Weeks`) { 245 248 t.Fatalf("expected cooked recipe older than four weeks to be hidden, got body: %s", body) 246 249 } 247 - if !strings.Contains(body, `action="/user/recipes/remove"`) { 248 - t.Fatalf("expected remove recipe form on past recipes list, got body: %s", body) 250 + if !strings.Contains(body, `hx-post="/user/recipes/remove"`) { 251 + t.Fatalf("expected remove recipe form to post via htmx, got body: %s", body) 252 + } 253 + if !strings.Contains(body, `hx-target="closest li"`) || !strings.Contains(body, `hx-swap="delete"`) { 254 + t.Fatalf("expected remove recipe form to delete only the matching row, got body: %s", body) 249 255 } 250 256 } 251 257 252 - func TestHandleRemoveUserRecipe_RemovesMatchingRecipe(t *testing.T) { 258 + func TestHandleRemoveUserRecipe_RejectsNonHTMXRequests(t *testing.T) { 253 259 t.Parallel() 254 260 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 255 261 storage := NewStorage(cacheStore) ··· 281 287 282 288 s.handleRemoveUserRecipe(rr, req) 283 289 284 - if rr.Code != http.StatusSeeOther { 285 - t.Fatalf("expected status %d, got %d", http.StatusSeeOther, rr.Code) 290 + if rr.Code != http.StatusBadRequest { 291 + t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rr.Code) 292 + } 293 + 294 + updated, err := storage.GetByID("user-1") 295 + if err != nil { 296 + t.Fatalf("failed to fetch updated user: %v", err) 297 + } 298 + if len(updated.LastRecipes) != 2 { 299 + t.Fatalf("expected recipes to remain unchanged, got %d", len(updated.LastRecipes)) 300 + } 301 + gotTitles := []string{updated.LastRecipes[0].Title, updated.LastRecipes[1].Title} 302 + if !strings.Contains(strings.Join(gotTitles, ","), keep.Title) || !strings.Contains(strings.Join(gotTitles, ","), remove.Title) { 303 + t.Fatalf("expected recipes to remain unchanged, got %#v", updated.LastRecipes) 286 304 } 287 - if got := rr.Header().Get("Location"); got != "/user?tab=past" { 288 - t.Fatalf("expected redirect to /user?tab=past, got %q", got) 305 + } 306 + 307 + func TestHandleRemoveUserRecipe_HTMXRemovesMatchingRecipeWithoutRedirect(t *testing.T) { 308 + t.Parallel() 309 + cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 310 + storage := NewStorage(cacheStore) 311 + s := &server{ 312 + storage: storage, 313 + userTmpl: templates.User, 314 + clerk: testAuthClient{}, 315 + } 316 + 317 + keep := utypes.Recipe{Title: "Keep Me", Hash: "hash-keep", CreatedAt: time.Now().Add(-2 * time.Hour).Round(0)} 318 + remove := utypes.Recipe{Title: "Remove Me", Hash: "hash-remove", CreatedAt: time.Now().Add(-1 * time.Hour).Round(0)} 319 + existing := &utypes.User{ 320 + ID: "user-1", 321 + Email: []string{"user@example.com"}, 322 + CreatedAt: time.Now(), 323 + ShoppingDay: "Saturday", 324 + LastRecipes: []utypes.Recipe{keep, remove}, 325 + } 326 + if err := storage.Update(existing); err != nil { 327 + t.Fatalf("failed to seed user: %v", err) 328 + } 329 + 330 + form := url.Values{ 331 + "hash": {remove.Hash}, 332 + } 333 + req := httptest.NewRequest(http.MethodPost, "/user/recipes/remove", strings.NewReader(form.Encode())) 334 + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 335 + req.Header.Set("HX-Request", "true") 336 + rr := httptest.NewRecorder() 337 + 338 + s.handleRemoveUserRecipe(rr, req) 339 + 340 + if rr.Code != http.StatusOK { 341 + t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code) 342 + } 343 + if got := rr.Header().Get("Location"); got != "" { 344 + t.Fatalf("expected no redirect location for htmx request, got %q", got) 289 345 } 290 346 291 347 updated, err := storage.GetByID("user-1")