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
|
### Product detail page
|
||||||
- [x] PDP image gallery: scroll-snap carousel with dots (mobile), thumbnails + arrows + lightbox (desktop)
|
- [x] PDP image gallery: scroll-snap carousel with dots (mobile), thumbnails + arrows + lightbox (desktop)
|
||||||
- [x] Product category breadcrumbs look bad — review styling/layout
|
- [x] Product category breadcrumbs look bad — review styling/layout
|
||||||
- [ ] Quantity selector on product page doesn't work
|
- [x] Quantity selector on product page doesn't work
|
||||||
- [ ] Trust badges: two different tick icons, should use sentence case not title case
|
- [x] Trust badges: two different tick icons, should use sentence case not title case
|
||||||
- [ ] Real product variants need testing and refinement with live data
|
- [ ] Real product variants need testing and refinement with live data
|
||||||
|
|
||||||
### Cart
|
### Cart
|
||||||
|
|||||||
@ -701,25 +701,24 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders trust badges (e.g., Free Delivery, Easy Returns).
|
Renders trust badges (e.g., free delivery, easy returns).
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
* `items` - Optional. List of badge items. Each item is a map with:
|
* `items` - Optional. List of badge items. Each item is a map with:
|
||||||
- `icon` - Icon type: `:check` or `:shield`
|
- `title` - Badge title (sentence case)
|
||||||
- `title` - Badge title
|
|
||||||
- `description` - Badge description
|
- `description` - Badge description
|
||||||
Defaults to Free Delivery and Easy Returns badges.
|
Defaults to "Made to order" and "Quality materials" badges.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
<.trust_badges />
|
<.trust_badges />
|
||||||
<.trust_badges items={[%{icon: :check, title: "Custom", description: "Badge text"}]} />
|
<.trust_badges items={[%{title: "Custom", description: "Badge text"}]} />
|
||||||
"""
|
"""
|
||||||
attr :items, :list,
|
attr :items, :list,
|
||||||
default: [
|
default: [
|
||||||
%{icon: :check, title: "Made to Order", description: "Printed just for you"},
|
%{title: "Made to order", description: "Printed just for you"},
|
||||||
%{icon: :shield, title: "Quality Materials", description: "Premium inks and substrates"}
|
%{title: "Quality materials", description: "Premium inks and substrates"}
|
||||||
]
|
]
|
||||||
|
|
||||||
def trust_badges(assigns) do
|
def trust_badges(assigns) do
|
||||||
@ -730,7 +729,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
|
|||||||
>
|
>
|
||||||
<%= for item <- @items do %>
|
<%= for item <- @items do %>
|
||||||
<div class="flex items-start gap-3">
|
<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>
|
<div>
|
||||||
<p class="font-semibold" style="color: var(--t-text-primary);">{item.title}</p>
|
<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>
|
<p class="text-sm" style="color: var(--t-text-secondary);">{item.description}</p>
|
||||||
@ -741,55 +745,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
|
|||||||
"""
|
"""
|
||||||
end
|
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 """
|
@doc """
|
||||||
Renders a customer reviews section with collapsible header and review cards.
|
Renders a customer reviews section with collapsible header and review cards.
|
||||||
|
|
||||||
|
|||||||
@ -1556,14 +1556,32 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
|||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
style="border: 2px solid var(--t-border-default); border-radius: var(--t-radius-input);"
|
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
|
<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);"
|
style="border-color: var(--t-border-default); color: var(--t-text-primary);"
|
||||||
>
|
>
|
||||||
{@quantity}
|
{@quantity}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
<%= if @in_stock do %>
|
<%= if @in_stock do %>
|
||||||
<span class="text-sm" style="color: var(--t-text-tertiary);">In stock</span>
|
<span class="text-sm" style="color: var(--t-text-tertiary);">In stock</span>
|
||||||
|
|||||||
@ -130,6 +130,18 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
|
|||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
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
|
@impl true
|
||||||
def handle_event("add_to_cart", _params, socket) do
|
def handle_event("add_to_cart", _params, socket) do
|
||||||
variant = socket.assigns.selected_variant
|
variant = socket.assigns.selected_variant
|
||||||
@ -140,6 +152,7 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
|
|||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> SimpleshopThemeWeb.CartHook.broadcast_and_update(cart)
|
|> SimpleshopThemeWeb.CartHook.broadcast_and_update(cart)
|
||||||
|
|> assign(:quantity, 1)
|
||||||
|> assign(:cart_drawer_open, true)
|
|> assign(:cart_drawer_open, true)
|
||||||
|> assign(:cart_status, "#{socket.assigns.product.name} added to cart")
|
|> assign(:cart_status, "#{socket.assigns.product.name} added to cart")
|
||||||
|
|
||||||
|
|||||||
@ -104,6 +104,67 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShowTest do
|
|||||||
end
|
end
|
||||||
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
|
describe "Add to cart" do
|
||||||
test "add to cart opens the cart drawer", %{conn: conn} do
|
test "add to cart opens the cart drawer", %{conn: conn} do
|
||||||
{:ok, view, _html} = live(conn, ~p"/products/1")
|
{:ok, view, _html} = live(conn, ~p"/products/1")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user