fix PDP quantity selector and trust badge consistency

Wire up +/− buttons with phx-click events and handle_event handlers,
clamp to 1–99, reset to 1 after add-to-cart. Trust badges now use a
single hero-check-circle icon and sentence case text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-02-10 23:15:09 +00:00
parent 8775c2eeef
commit 3c73b98d2b
5 changed files with 109 additions and 62 deletions

View File

@ -43,8 +43,8 @@ Issues found during hands-on testing of the deployed prod site on mobile and des
### Product detail page
- [x] PDP image gallery: scroll-snap carousel with dots (mobile), thumbnails + arrows + lightbox (desktop)
- [x] Product category breadcrumbs look bad — review styling/layout
- [ ] Quantity selector on product page doesn't work
- [ ] Trust badges: two different tick icons, should use sentence case not title case
- [x] Quantity selector on product page doesn't work
- [x] Trust badges: two different tick icons, should use sentence case not title case
- [ ] Real product variants need testing and refinement with live data
### Cart

View File

@ -701,25 +701,24 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
end
@doc """
Renders trust badges (e.g., Free Delivery, Easy Returns).
Renders trust badges (e.g., free delivery, easy returns).
## Attributes
* `items` - Optional. List of badge items. Each item is a map with:
- `icon` - Icon type: `:check` or `:shield`
- `title` - Badge title
- `title` - Badge title (sentence case)
- `description` - Badge description
Defaults to Free Delivery and Easy Returns badges.
Defaults to "Made to order" and "Quality materials" badges.
## Examples
<.trust_badges />
<.trust_badges items={[%{icon: :check, title: "Custom", description: "Badge text"}]} />
<.trust_badges items={[%{title: "Custom", description: "Badge text"}]} />
"""
attr :items, :list,
default: [
%{icon: :check, title: "Made to Order", description: "Printed just for you"},
%{icon: :shield, title: "Quality Materials", description: "Premium inks and substrates"}
%{title: "Made to order", description: "Printed just for you"},
%{title: "Quality materials", description: "Premium inks and substrates"}
]
def trust_badges(assigns) do
@ -730,7 +729,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
>
<%= for item <- @items do %>
<div class="flex items-start gap-3">
<.trust_badge_icon icon={item.icon} />
<span style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
<SimpleshopThemeWeb.CoreComponents.icon
name="hero-check-circle"
class="size-5 mt-0.5 shrink-0"
/>
</span>
<div>
<p class="font-semibold" style="color: var(--t-text-primary);">{item.title}</p>
<p class="text-sm" style="color: var(--t-text-secondary);">{item.description}</p>
@ -741,55 +745,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
"""
end
attr :icon, :atom, required: true
defp trust_badge_icon(%{icon: :check} = assigns) do
~H"""
<svg
class="w-5 h-5 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
"""
end
defp trust_badge_icon(%{icon: :shield} = assigns) do
~H"""
<svg
class="w-5 h-5 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
"""
end
defp trust_badge_icon(assigns) do
~H"""
<svg
class="w-5 h-5 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
"""
end
@doc """
Renders a customer reviews section with collapsible header and review cards.

View File

@ -1556,14 +1556,32 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
class="flex items-center"
style="border: 2px solid var(--t-border-default); border-radius: var(--t-radius-input);"
>
<button type="button" class="px-4 py-2" style="color: var(--t-text-primary);"></button>
<button
type="button"
phx-click="decrement_quantity"
disabled={@quantity <= @min}
aria-label="Decrease quantity"
class="px-4 py-2 disabled:opacity-30"
style="color: var(--t-text-primary);"
>
</button>
<span
class="px-4 py-2 border-x-2"
class="px-4 py-2 border-x-2 min-w-12 text-center tabular-nums"
style="border-color: var(--t-border-default); color: var(--t-text-primary);"
>
{@quantity}
</span>
<button type="button" class="px-4 py-2" style="color: var(--t-text-primary);">+</button>
<button
type="button"
phx-click="increment_quantity"
disabled={@quantity >= @max}
aria-label="Increase quantity"
class="px-4 py-2 disabled:opacity-30"
style="color: var(--t-text-primary);"
>
+
</button>
</div>
<%= if @in_stock do %>
<span class="text-sm" style="color: var(--t-text-tertiary);">In stock</span>

View File

@ -130,6 +130,18 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
{:noreply, socket}
end
@impl true
def handle_event("increment_quantity", _params, socket) do
quantity = min(socket.assigns.quantity + 1, 99)
{:noreply, assign(socket, :quantity, quantity)}
end
@impl true
def handle_event("decrement_quantity", _params, socket) do
quantity = max(socket.assigns.quantity - 1, 1)
{:noreply, assign(socket, :quantity, quantity)}
end
@impl true
def handle_event("add_to_cart", _params, socket) do
variant = socket.assigns.selected_variant
@ -140,6 +152,7 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
socket =
socket
|> SimpleshopThemeWeb.CartHook.broadcast_and_update(cart)
|> assign(:quantity, 1)
|> assign(:cart_drawer_open, true)
|> assign(:cart_status, "#{socket.assigns.product.name} added to cart")

View File

@ -104,6 +104,67 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShowTest do
end
end
describe "Quantity selector" do
test "renders quantity selector with initial value of 1", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
assert has_element?(view, "button[phx-click='decrement_quantity']")
assert has_element?(view, "button[phx-click='increment_quantity']")
end
test "decrement button is disabled at quantity 1", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
assert has_element?(view, "button[phx-click='decrement_quantity'][disabled]")
end
test "increment increases quantity", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
html =
view
|> element("button[phx-click='increment_quantity']")
|> render_click()
# Quantity should now be 2, decrement no longer disabled
refute html =~ ~s(phx-click="decrement_quantity" disabled)
end
test "decrement decreases quantity", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
# Increment twice to get to 3
view |> element("button[phx-click='increment_quantity']") |> render_click()
view |> element("button[phx-click='increment_quantity']") |> render_click()
html =
view
|> element("button[phx-click='decrement_quantity']")
|> render_click()
# Should be back to 2 — decrement still enabled
refute html =~ ~s(phx-click="decrement_quantity" disabled)
end
test "quantity resets to 1 after adding to cart", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")
# Increment to 3
view |> element("button[phx-click='increment_quantity']") |> render_click()
view |> element("button[phx-click='increment_quantity']") |> render_click()
# Add to cart
html =
view
|> element("button", "Add to basket")
|> render_click()
# Decrement should be disabled again (quantity reset to 1)
assert html =~ ~s(phx-click="decrement_quantity")
assert html =~ "disabled"
end
end
describe "Add to cart" do
test "add to cart opens the cart drawer", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/products/1")