add no-JS progressive enhancement for all shop flows
All checks were successful
deploy / deploy (push) Successful in 1m23s

Every key shop flow now works via plain HTML forms when JS is
unavailable. LiveView progressively enhances when JS connects.

- PDP: form wraps variant/qty/add-to-cart with action="/cart/add"
- Cart page: qty +/- and remove use form POST fallbacks
- Cart/search header icons are now links with phx-click enhancement
- Collection sort form has GET action + noscript submit button
- New /search page with form-based search for no-JS users
- CartController gains add/remove/update_item POST actions
- CartHook gains update_quantity_form and remove_item_form handlers
- Fix flaky analytics tests caused by event table pollution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-24 22:56:19 +00:00
parent f788108665
commit 0b0adba0fe
16 changed files with 461 additions and 67 deletions

View File

@@ -1,9 +1,10 @@
defmodule BerrypodWeb.CartController do
@moduledoc """
API controller for cart session persistence.
Cart controller handling both JSON API persistence and HTML form fallbacks.
LiveView cannot write to session directly, so cart updates are persisted
via this API endpoint called from a JS hook after each cart modification.
The JSON `update/2` action is called by a JS hook after each LiveView cart
change. The HTML actions (`add/2`, `remove/2`, `update_item/2`) provide
no-JS fallbacks via plain form POST + redirect.
"""
use BerrypodWeb, :controller
@@ -11,9 +12,7 @@ defmodule BerrypodWeb.CartController do
alias Berrypod.Cart
@doc """
Updates the cart in session.
Expects JSON body with `items` as a list of [variant_id, quantity] arrays.
Updates the cart in session (JSON API for JS hook).
"""
def update(conn, %{"items" => items}) when is_list(items) do
cart_items = Cart.deserialize(items)
@@ -28,4 +27,69 @@ defmodule BerrypodWeb.CartController do
|> put_status(:bad_request)
|> json(%{error: "Invalid cart data"})
end
@doc """
Adds an item to cart via form POST (no-JS fallback).
"""
def add(conn, %{"variant_id" => variant_id, "quantity" => qty_str}) do
quantity = parse_quantity(qty_str)
cart = Cart.get_from_session(get_session(conn))
cart = Cart.add_item(cart, variant_id, quantity)
conn
|> Cart.put_in_session(cart)
|> put_flash(:info, "Added to basket")
|> redirect(to: ~p"/cart")
end
def add(conn, _params) do
conn
|> put_flash(:error, "Could not add item to basket")
|> redirect(to: ~p"/cart")
end
@doc """
Removes an item from cart via form POST (no-JS fallback).
"""
def remove(conn, %{"variant_id" => variant_id}) do
cart = Cart.get_from_session(get_session(conn))
cart = Cart.remove_item(cart, variant_id)
conn
|> Cart.put_in_session(cart)
|> put_flash(:info, "Removed from basket")
|> redirect(to: ~p"/cart")
end
@doc """
Updates item quantity via form POST (no-JS fallback).
"""
def update_item(conn, %{"variant_id" => variant_id, "quantity" => qty_str}) do
quantity = parse_update_quantity(qty_str)
cart = Cart.get_from_session(get_session(conn))
cart = Cart.update_quantity(cart, variant_id, quantity)
conn
|> Cart.put_in_session(cart)
|> redirect(to: ~p"/cart")
end
defp parse_quantity(str) when is_binary(str) do
case Integer.parse(str) do
{qty, _} when qty > 0 -> qty
_ -> 1
end
end
defp parse_quantity(_), do: 1
# Allows 0 and negative values so Cart.update_quantity can remove items
defp parse_update_quantity(str) when is_binary(str) do
case Integer.parse(str) do
{qty, _} -> qty
:error -> 1
end
end
defp parse_update_quantity(_), do: 1
end