make PDP variant selection work without JS
All checks were successful
deploy / deploy (push) Successful in 1m3s
All checks were successful
deploy / deploy (push) Successful in 1m3s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -452,6 +452,7 @@ defmodule BerrypodWeb.Admin.Theme.Index do
|
||||
|> assign(:available_options, available_options)
|
||||
|> assign(:display_price, display_price)
|
||||
|> assign(:quantity, 1)
|
||||
|> assign(:option_urls, %{})
|
||||
|
||||
~H"<BerrypodWeb.PageTemplates.pdp {assigns} />"
|
||||
end
|
||||
|
||||
@@ -36,10 +36,6 @@ defmodule BerrypodWeb.Shop.ProductShow do
|
||||
|
||||
option_types = Product.option_types(product)
|
||||
variants = product.variants || []
|
||||
{selected_options, selected_variant} = initialize_variant_selection(variants)
|
||||
available_options = compute_available_options(option_types, variants, selected_options)
|
||||
display_price = variant_price(selected_variant, product)
|
||||
gallery_images = filter_gallery_images(all_images, selected_options["Color"])
|
||||
|
||||
if connected?(socket) and socket.assigns[:analytics_visitor_hash] do
|
||||
Analytics.track_event(
|
||||
@@ -62,26 +58,105 @@ defmodule BerrypodWeb.Shop.ProductShow do
|
||||
|> assign(:json_ld, product_json_ld(product, og_url, og_image, base))
|
||||
|> assign(:product, product)
|
||||
|> assign(:all_images, all_images)
|
||||
|> assign(:gallery_images, gallery_images)
|
||||
|> assign(:related_products, related_products)
|
||||
|> assign(:quantity, 1)
|
||||
|> assign(:option_types, option_types)
|
||||
|> assign(:variants, variants)
|
||||
|> assign(:selected_options, selected_options)
|
||||
|> assign(:selected_variant, selected_variant)
|
||||
|> assign(:available_options, available_options)
|
||||
|> assign(:display_price, display_price)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp initialize_variant_selection([first | _] = _variants) do
|
||||
{first.options, first}
|
||||
@impl true
|
||||
def handle_params(params, _uri, socket) do
|
||||
if socket.assigns[:product] do
|
||||
{:noreply, apply_variant_params(params, socket)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp initialize_variant_selection([]) do
|
||||
{%{}, nil}
|
||||
defp apply_variant_params(params, socket) do
|
||||
%{option_types: option_types, variants: variants, product: product, all_images: all_images} =
|
||||
socket.assigns
|
||||
|
||||
option_names = Enum.map(option_types, & &1.name)
|
||||
|
||||
default_options =
|
||||
case variants do
|
||||
[first | _] -> first.options
|
||||
[] -> %{}
|
||||
end
|
||||
|
||||
# Extract option params from the URL, ignoring unknown keys
|
||||
param_options =
|
||||
Enum.reduce(option_names, %{}, fn name, acc ->
|
||||
case params[name] do
|
||||
nil -> acc
|
||||
value -> Map.put(acc, name, value)
|
||||
end
|
||||
end)
|
||||
|
||||
{selected_options, changed_option} =
|
||||
if map_size(param_options) == 0 do
|
||||
{default_options, nil}
|
||||
else
|
||||
merged = Map.merge(default_options, param_options)
|
||||
|
||||
# Identify which option was changed from the default
|
||||
changed =
|
||||
Enum.find(option_names, fn name ->
|
||||
Map.has_key?(param_options, name) and param_options[name] != default_options[name]
|
||||
end)
|
||||
|
||||
{merged, changed}
|
||||
end
|
||||
|
||||
selected_options =
|
||||
if changed_option do
|
||||
resolve_valid_combo(variants, option_types, selected_options, changed_option)
|
||||
else
|
||||
selected_options
|
||||
end
|
||||
|
||||
selected_variant = find_variant(variants, selected_options)
|
||||
|
||||
# If params produced an invalid combo with no matching variant, fall back to defaults
|
||||
{selected_options, selected_variant} =
|
||||
if is_nil(selected_variant) and variants != [] do
|
||||
opts = List.first(variants).options
|
||||
{opts, List.first(variants)}
|
||||
else
|
||||
{selected_options, selected_variant}
|
||||
end
|
||||
|
||||
available_options = compute_available_options(option_types, variants, selected_options)
|
||||
display_price = variant_price(selected_variant, product)
|
||||
gallery_images = filter_gallery_images(all_images, selected_options["Color"])
|
||||
|
||||
option_urls = build_option_urls(option_types, selected_options, product.slug)
|
||||
|
||||
socket
|
||||
|> assign(:selected_options, selected_options)
|
||||
|> assign(:selected_variant, selected_variant)
|
||||
|> assign(:available_options, available_options)
|
||||
|> assign(:display_price, display_price)
|
||||
|> assign(:gallery_images, gallery_images)
|
||||
|> assign(:option_urls, option_urls)
|
||||
end
|
||||
|
||||
defp build_option_urls(option_types, selected_options, slug) do
|
||||
Enum.reduce(option_types, %{}, fn opt_type, acc ->
|
||||
urls =
|
||||
opt_type.values
|
||||
|> Enum.map(fn value ->
|
||||
params = Map.put(selected_options, opt_type.name, value.title)
|
||||
{value.title, ~p"/products/#{slug}?#{params}"}
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
Map.put(acc, opt_type.name, urls)
|
||||
end)
|
||||
end
|
||||
|
||||
defp compute_available_options(option_types, variants, selected_options) do
|
||||
@@ -148,32 +223,6 @@ defmodule BerrypodWeb.Shop.ProductShow do
|
||||
|> Enum.map(& &1.url)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("select_option", %{"option" => option_name, "selected" => value}, socket) do
|
||||
variants = socket.assigns.variants
|
||||
option_types = socket.assigns.option_types
|
||||
|
||||
selected_options = Map.put(socket.assigns.selected_options, option_name, value)
|
||||
selected_options = resolve_valid_combo(variants, option_types, selected_options, option_name)
|
||||
|
||||
selected_variant = find_variant(variants, selected_options)
|
||||
|
||||
available_options =
|
||||
compute_available_options(option_types, variants, selected_options)
|
||||
|
||||
gallery_images = filter_gallery_images(socket.assigns.all_images, selected_options["Color"])
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:selected_options, selected_options)
|
||||
|> assign(:selected_variant, selected_variant)
|
||||
|> assign(:available_options, available_options)
|
||||
|> assign(:display_price, variant_price(selected_variant, socket.assigns.product))
|
||||
|> assign(:gallery_images, gallery_images)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("increment_quantity", _params, socket) do
|
||||
quantity = min(socket.assigns.quantity + 1, 99)
|
||||
|
||||
Reference in New Issue
Block a user