From 23e95a3de697ad537c71b84e65863895dda5eb0d Mon Sep 17 00:00:00 2001 From: jamey Date: Wed, 25 Feb 2026 01:08:36 +0000 Subject: [PATCH] make PDP variant selection work without JS Variant options (colour, size) are now URL params handled via handle_params instead of phx-click events. Swatches and size buttons render as patch links in shop mode, so changing variants works as plain navigation without JS. Quantity is now a number input that submits with the form. Unavailable variants render as disabled spans. Co-Authored-By: Claude Opus 4.6 --- assets/css/shop/components.css | 27 +++- .../components/page_templates/pdp.html.heex | 3 +- .../components/shop_components/product.ex | 102 +++++++++----- lib/berrypod_web/live/admin/theme/index.ex | 1 + lib/berrypod_web/live/shop/product_show.ex | 127 ++++++++++++------ .../live/shop/product_show_test.exs | 53 +++++++- 6 files changed, 238 insertions(+), 75 deletions(-) diff --git a/assets/css/shop/components.css b/assets/css/shop/components.css index 6b5abe4..dac6456 100644 --- a/assets/css/shop/components.css +++ b/assets/css/shop/components.css @@ -578,11 +578,18 @@ position: relative; cursor: pointer; padding: 0; + display: inline-block; + text-decoration: none; &[aria-pressed="true"] { border-color: var(--t-accent); --tw-ring-color: var(--t-accent); } + + &[aria-disabled="true"] { + opacity: 0.3; + cursor: not-allowed; + } } .size-btn { @@ -594,11 +601,18 @@ font-weight: 500; transition: all 0.2s ease; cursor: pointer; + text-decoration: none; &[aria-pressed="true"] { border-color: var(--t-accent); background: color-mix(in oklch, var(--t-accent) 10%, transparent); } + + &[aria-disabled="true"] { + opacity: 0.3; + cursor: not-allowed; + border-style: dashed; + } } /* ── Quantity selector ── */ @@ -643,9 +657,20 @@ color: var(--t-text-primary); padding: 0.5rem 1rem; border-inline: 2px solid var(--t-border-default); - min-width: 3rem; + width: 3.5rem; text-align: center; font-variant-numeric: tabular-nums; + background: none; + border-top: none; + border-bottom: none; + font: inherit; + appearance: textfield; + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + appearance: none; + margin: 0; + } } .stock-in { diff --git a/lib/berrypod_web/components/page_templates/pdp.html.heex b/lib/berrypod_web/components/page_templates/pdp.html.heex index 7e82d79..59475c0 100644 --- a/lib/berrypod_web/components/page_templates/pdp.html.heex +++ b/lib/berrypod_web/components/page_templates/pdp.html.heex @@ -32,7 +32,7 @@ name="variant_id" value={@selected_variant && @selected_variant.id} /> - + <%!-- quantity is provided by the quantity_selector input below --%> <%!-- Dynamic variant selectors --%> <%= for option_type <- @option_types do %> @@ -41,6 +41,7 @@ selected={@selected_options[option_type.name]} available={@available_options[option_type.name] || []} mode={@mode} + option_urls={(@option_urls || %{})[option_type.name] || %{}} /> <% end %> diff --git a/lib/berrypod_web/components/shop_components/product.ex b/lib/berrypod_web/components/shop_components/product.ex index 4670e7f..f250d6b 100644 --- a/lib/berrypod_web/components/shop_components/product.ex +++ b/lib/berrypod_web/components/shop_components/product.ex @@ -1339,13 +1339,15 @@ defmodule BerrypodWeb.ShopComponents.Product do Renders a variant selector for a single option type. Shows color swatches for color-type options, text buttons for others. - Disables unavailable options and fires `select_option` event on click. + In shop mode, each option is a patch link for progressive enhancement. + In preview mode, options are inert buttons. ## Attributes * `option_type` - Required. Map with :name, :type, :values keys * `selected` - Required. Currently selected value (string) * `available` - Required. List of available values for this option + * `option_urls` - Optional. Map of value title => patch URL * `mode` - Optional. :shop or :preview (default: :shop) ## Examples @@ -1354,11 +1356,13 @@ defmodule BerrypodWeb.ShopComponents.Product do option_type={%{name: "Size", type: :size, values: [%{title: "S"}, ...]}} selected="M" available={["S", "M", "L"]} + option_urls={%{"S" => "/products/foo?Size=S", ...}} /> """ attr :option_type, :map, required: true attr :selected, :string, required: true attr :available, :list, required: true + attr :option_urls, :map, default: %{} attr :mode, :atom, default: :shop def variant_selector(assigns) do @@ -1378,8 +1382,8 @@ defmodule BerrypodWeb.ShopComponents.Product do hex={value[:hex] || "#888888"} selected={value.title == @selected} disabled={value.title not in @available} - option_name={@option_type.name} mode={@mode} + url={@option_urls[value.title]} /> <% else %> <.size_button @@ -1387,8 +1391,8 @@ defmodule BerrypodWeb.ShopComponents.Product do title={value.title} selected={value.title == @selected} disabled={value.title not in @available} - option_name={@option_type.name} mode={@mode} + url={@option_urls[value.title]} /> <% end %> @@ -1400,44 +1404,76 @@ defmodule BerrypodWeb.ShopComponents.Product do attr :hex, :string, required: true attr :selected, :boolean, required: true attr :disabled, :boolean, required: true - attr :option_name, :string, required: true attr :mode, :atom, default: :shop + attr :url, :string, default: nil defp color_swatch(assigns) do ~H""" - + <%= cond do %> + <% @mode == :shop and not @disabled -> %> + <.link + patch={@url} + class="color-swatch" + style={"background-color: #{@hex};"} + title={@title} + aria-label={"Select #{@title}"} + aria-pressed={to_string(@selected)} + > + + <% @mode == :shop -> %> + + <% true -> %> + + <%= cond do %> + <% @mode == :shop and not @disabled -> %> + <.link + patch={@url} + class="size-btn" + aria-pressed={to_string(@selected)} + > + {@title} + + <% @mode == :shop -> %> + + {@title} + + <% true -> %> + + <% end %> """ end @@ -1478,9 +1514,15 @@ defmodule BerrypodWeb.ShopComponents.Product do > − - - {@quantity} - +