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:
parent
8775c2eeef
commit
3c73b98d2b
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user