add per-colour product images and gallery colour filtering
Tag product images with their colour during sync (both Printful and Printify providers). Printify images are cherry-picked: hero colour keeps all angles, other colours keep front + back only. Printful MockupEnricher now generates mockups per colour from the color_variant_map. PDP gallery filters by the selected colour, falling back to all images when the selected colour has none. Fix option name mismatch (Printify "Colors" vs variant "Color") by singularizing in Product.option_types. Generator creates multi-colour apparel products so mock data matches real sync behaviour. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1530,23 +1530,18 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
~H"""
|
||||
<button
|
||||
type="button"
|
||||
phx-click={if @mode == :shop and not @disabled, do: "select_option"}
|
||||
phx-click={if @mode == :shop, do: "select_option"}
|
||||
phx-value-option={@option_name}
|
||||
phx-value-value={@title}
|
||||
phx-value-selected={@title}
|
||||
class={[
|
||||
"w-10 h-10 rounded-full border-2 transition-all relative",
|
||||
@selected && "ring-2 ring-offset-2",
|
||||
@disabled && "opacity-40 cursor-not-allowed"
|
||||
@selected && "ring-2 ring-offset-2"
|
||||
]}
|
||||
style={"background-color: #{@hex}; border-color: #{if @selected, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l))", else: "var(--t-border-default)"}; --tw-ring-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"}
|
||||
title={@title}
|
||||
disabled={@disabled}
|
||||
aria-label={"Select #{@title}"}
|
||||
aria-pressed={@selected}
|
||||
aria-pressed={to_string(@selected)}
|
||||
>
|
||||
<span :if={@disabled} class="absolute inset-0 flex items-center justify-center">
|
||||
<span class="w-full h-0.5 bg-gray-400 rotate-45 absolute"></span>
|
||||
</span>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
@@ -1561,16 +1556,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
~H"""
|
||||
<button
|
||||
type="button"
|
||||
phx-click={if @mode == :shop and not @disabled, do: "select_option"}
|
||||
phx-click={if @mode == :shop, do: "select_option"}
|
||||
phx-value-option={@option_name}
|
||||
phx-value-value={@title}
|
||||
phx-value-selected={@title}
|
||||
class={[
|
||||
"px-4 py-2 font-medium transition-all",
|
||||
@disabled && "opacity-40 cursor-not-allowed line-through"
|
||||
"px-4 py-2 font-medium transition-all"
|
||||
]}
|
||||
style={"border: 2px solid #{if @selected, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l))", else: "var(--t-border-default)"}; border-radius: var(--t-radius-button); color: var(--t-text-primary); background: #{if @selected, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1)", else: "transparent"};"}
|
||||
disabled={@disabled}
|
||||
aria-pressed={@selected}
|
||||
aria-pressed={to_string(@selected)}
|
||||
>
|
||||
{@title}
|
||||
</button>
|
||||
|
||||
@@ -19,22 +19,26 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
|
||||
exclude: product.id
|
||||
)
|
||||
|
||||
gallery_images =
|
||||
all_images =
|
||||
(product.images || [])
|
||||
|> Enum.sort_by(& &1.position)
|
||||
|> Enum.map(fn img -> ProductImage.direct_url(img, 1200) end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.map(fn img ->
|
||||
%{url: ProductImage.direct_url(img, 1200), color: img.color}
|
||||
end)
|
||||
|> Enum.reject(fn img -> is_nil(img.url) end)
|
||||
|
||||
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"])
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:page_title, product.title)
|
||||
|> assign(:product, product)
|
||||
|> assign(:all_images, all_images)
|
||||
|> assign(:gallery_images, gallery_images)
|
||||
|> assign(:related_products, related_products)
|
||||
|> assign(:quantity, 1)
|
||||
@@ -80,22 +84,61 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
|
||||
defp variant_price(_, %{cheapest_price: price}), do: price
|
||||
defp variant_price(_, _), do: 0
|
||||
|
||||
# If the current combo doesn't match any variant, auto-adjust other options
|
||||
# to find a valid one. Keeps the just-changed option fixed, adjusts the rest.
|
||||
defp resolve_valid_combo(variants, option_types, selected_options, changed_option) do
|
||||
if Enum.any?(variants, fn v -> v.options == selected_options end) do
|
||||
selected_options
|
||||
else
|
||||
matching =
|
||||
Enum.filter(variants, fn v ->
|
||||
v.is_available && v.options[changed_option] == selected_options[changed_option]
|
||||
end)
|
||||
|
||||
case matching do
|
||||
[first | _] ->
|
||||
Enum.reduce(option_types, selected_options, fn opt_type, acc ->
|
||||
if opt_type.name == changed_option do
|
||||
acc
|
||||
else
|
||||
Map.put(acc, opt_type.name, first.options[opt_type.name])
|
||||
end
|
||||
end)
|
||||
|
||||
[] ->
|
||||
selected_options
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp find_variant(variants, selected_options) do
|
||||
Enum.find(variants, fn v -> v.options == selected_options end)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("select_option", %{"option" => option_name, "value" => value}, socket) do
|
||||
selected_options = Map.put(socket.assigns.selected_options, option_name, value)
|
||||
defp filter_gallery_images(all_images, selected_color) do
|
||||
if selected_color do
|
||||
color_images = Enum.filter(all_images, &(&1.color == selected_color))
|
||||
if color_images == [], do: all_images, else: color_images
|
||||
else
|
||||
all_images
|
||||
end
|
||||
|> Enum.map(& &1.url)
|
||||
end
|
||||
|
||||
selected_variant = find_variant(socket.assigns.variants, selected_options)
|
||||
@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(
|
||||
socket.assigns.option_types,
|
||||
socket.assigns.variants,
|
||||
selected_options
|
||||
)
|
||||
compute_available_options(option_types, variants, selected_options)
|
||||
|
||||
gallery_images = filter_gallery_images(socket.assigns.all_images, selected_options["Color"])
|
||||
|
||||
socket =
|
||||
socket
|
||||
@@ -103,6 +146,7 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
|
||||
|> 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
|
||||
|
||||
Reference in New Issue
Block a user