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

@@ -25,26 +25,36 @@
<div>
<.product_info product={@product} display_price={@display_price} />
<%!-- Dynamic variant selectors --%>
<%= for option_type <- @option_types do %>
<.variant_selector
option_type={option_type}
selected={@selected_options[option_type.name]}
available={@available_options[option_type.name] || []}
mode={@mode}
<form action="/cart/add" method="post" phx-submit="add_to_cart">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<input
type="hidden"
name="variant_id"
value={@selected_variant && @selected_variant.id}
/>
<% end %>
<input type="hidden" name="quantity" value={@quantity} />
<%!-- Fallback for products with no variant options --%>
<div
:if={@option_types == []}
class="pdp-variant-fallback"
>
One size
</div>
<%!-- Dynamic variant selectors --%>
<%= for option_type <- @option_types do %>
<.variant_selector
option_type={option_type}
selected={@selected_options[option_type.name]}
available={@available_options[option_type.name] || []}
mode={@mode}
/>
<% end %>
<.quantity_selector quantity={@quantity} in_stock={@product.in_stock} />
<.add_to_cart_button mode={@mode} />
<%!-- Fallback for products with no variant options --%>
<div
:if={@option_types == []}
class="pdp-variant-fallback"
>
One size
</div>
<.quantity_selector quantity={@quantity} in_stock={@product.in_stock} />
<.add_to_cart_button mode={@mode} />
</form>
<.trust_badges :if={@theme_settings.pdp_trust_badges} />
<.product_details product={@product} />
</div>

View File

@@ -213,11 +213,18 @@ defmodule BerrypodWeb.ShopComponents.Cart do
<div class="cart-item-actions">
<%= if @show_quantity_controls do %>
<div class="cart-qty-group">
<form
action="/cart/update"
method="post"
phx-submit="update_quantity_form"
class="cart-qty-group"
>
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<input type="hidden" name="variant_id" value={@item.variant_id} />
<button
type="button"
phx-click="decrement"
phx-value-id={@item.variant_id}
type="submit"
name="quantity"
value={@item.quantity - 1}
class="cart-qty-btn"
aria-label={"Decrease quantity of #{@item.name}"}
>
@@ -227,15 +234,15 @@ defmodule BerrypodWeb.ShopComponents.Cart do
{@item.quantity}
</span>
<button
type="button"
phx-click="increment"
phx-value-id={@item.variant_id}
type="submit"
name="quantity"
value={@item.quantity + 1}
class="cart-qty-btn"
aria-label={"Increase quantity of #{@item.name}"}
>
+
</button>
</div>
</form>
<% else %>
<span class="cart-qty-text">
Qty: {@item.quantity}
@@ -306,15 +313,17 @@ defmodule BerrypodWeb.ShopComponents.Cart do
def cart_remove_button(assigns) do
~H"""
<button
type="button"
phx-click="remove_item"
phx-value-id={@variant_id}
class="cart-remove-btn"
aria-label={"Remove #{@item_name} from cart"}
>
Remove
</button>
<form action="/cart/remove" method="post" phx-submit="remove_item_form" class="cart-remove-form">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<input type="hidden" name="variant_id" value={@variant_id} />
<button
type="submit"
class="cart-remove-btn"
aria-label={"Remove #{@item_name} from cart"}
>
Remove
</button>
</form>
"""
end

View File

@@ -755,10 +755,10 @@ defmodule BerrypodWeb.ShopComponents.Layout do
/>
</svg>
</.link>
<button
type="button"
class="header-icon-btn"
<a
href="/search"
phx-click={Phoenix.LiveView.JS.dispatch("open-search", to: "#search-modal")}
class="header-icon-btn"
aria-label="Search"
>
<svg
@@ -772,11 +772,11 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<circle cx="11" cy="11" r="8"></circle>
<path d="M21 21l-4.35-4.35"></path>
</svg>
</button>
<button
type="button"
class="header-icon-btn"
</a>
<a
href="/cart"
phx-click={open_cart_drawer_js()}
class="header-icon-btn"
aria-label="Cart"
>
<svg
@@ -797,7 +797,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
</span>
<% end %>
<span class="sr-only">Cart ({@cart_count})</span>
</button>
</a>
</div>
</header>
"""

View File

@@ -1525,8 +1525,8 @@ defmodule BerrypodWeb.ShopComponents.Product do
~H"""
<div class="atc-wrap" data-sticky={to_string(@sticky)}>
<button
type="button"
phx-click={if @mode == :preview, do: open_cart_drawer_js(), else: "add_to_cart"}
type={if @mode == :live, do: "submit", else: "button"}
phx-click={if @mode == :preview, do: open_cart_drawer_js()}
disabled={@disabled}
class="atc-btn"
>