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:
@@ -300,6 +300,12 @@ defmodule SimpleshopTheme.Providers.Printful do
|
||||
catalog_product_id = extract_catalog_product_id_from_variants(sync_variants)
|
||||
catalog_variant_ids = Enum.map(sync_variants, & &1["variant_id"]) |> Enum.reject(&is_nil/1)
|
||||
|
||||
color_variant_map =
|
||||
sync_variants
|
||||
|> Enum.reject(fn sv -> is_nil(sv["color"]) end)
|
||||
|> Enum.uniq_by(fn sv -> sv["color"] end)
|
||||
|> Map.new(fn sv -> {normalize_text(sv["color"]), sv["variant_id"]} end)
|
||||
|
||||
%{
|
||||
provider_product_id: to_string(sync_product["id"]),
|
||||
title: sync_product["name"],
|
||||
@@ -310,6 +316,7 @@ defmodule SimpleshopTheme.Providers.Printful do
|
||||
provider_data: %{
|
||||
catalog_product_id: catalog_product_id,
|
||||
catalog_variant_ids: catalog_variant_ids,
|
||||
color_variant_map: color_variant_map,
|
||||
# Shipping calc uses these generic keys (shared with Printify)
|
||||
blueprint_id: catalog_product_id,
|
||||
print_provider_id: 0,
|
||||
@@ -335,14 +342,14 @@ defmodule SimpleshopTheme.Providers.Printful do
|
||||
end
|
||||
|
||||
defp build_variant_title(sv) do
|
||||
parts = [sv["color"], sv["size"]] |> Enum.reject(&is_nil/1)
|
||||
parts = [sv["color"], sv["size"]] |> Enum.reject(&is_nil/1) |> Enum.map(&normalize_text/1)
|
||||
Enum.join(parts, " / ")
|
||||
end
|
||||
|
||||
defp build_variant_options(sv) do
|
||||
opts = %{}
|
||||
opts = if sv["color"], do: Map.put(opts, "Color", sv["color"]), else: opts
|
||||
opts = if sv["size"], do: Map.put(opts, "Size", sv["size"]), else: opts
|
||||
opts = if sv["color"], do: Map.put(opts, "Color", normalize_text(sv["color"])), else: opts
|
||||
opts = if sv["size"], do: Map.put(opts, "Size", normalize_text(sv["size"])), else: opts
|
||||
opts
|
||||
end
|
||||
|
||||
@@ -364,7 +371,8 @@ defmodule SimpleshopTheme.Providers.Printful do
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {img, index} ->
|
||||
alt = if img.color not in [nil, ""], do: img.color, else: img.name
|
||||
%{src: img.src, position: index, alt: alt}
|
||||
color = if img.color not in [nil, ""], do: normalize_text(img.color), else: nil
|
||||
%{src: img.src, position: index, alt: alt, color: color}
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -389,17 +397,21 @@ defmodule SimpleshopTheme.Providers.Printful do
|
||||
|
||||
# Build option types from variants for frontend display
|
||||
defp build_option_types(sync_variants) do
|
||||
# Build colour values with hex codes from sync variant data
|
||||
colors =
|
||||
sync_variants
|
||||
|> Enum.map(& &1["color"])
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.uniq()
|
||||
|> Enum.map(fn color -> %{"title" => color} end)
|
||||
|> Enum.reject(fn sv -> is_nil(sv["color"]) end)
|
||||
|> Enum.uniq_by(fn sv -> sv["color"] end)
|
||||
|> Enum.map(fn sv ->
|
||||
base = %{"title" => normalize_text(sv["color"])}
|
||||
if sv["color_code"], do: Map.put(base, "hex", sv["color_code"]), else: base
|
||||
end)
|
||||
|
||||
sizes =
|
||||
sync_variants
|
||||
|> Enum.map(& &1["size"])
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.map(&normalize_text/1)
|
||||
|> Enum.uniq()
|
||||
|> Enum.map(fn size -> %{"title" => size} end)
|
||||
|
||||
@@ -609,6 +621,17 @@ defmodule SimpleshopTheme.Providers.Printful do
|
||||
defp parse_int(value) when is_integer(value), do: value
|
||||
defp parse_int(value) when is_binary(value), do: String.to_integer(value)
|
||||
|
||||
# Printful uses Unicode double prime (″) and multiplication sign (×) in size
|
||||
# labels. These break LiveView's phx-value-* attribute serialization, so we
|
||||
# strip inch marks and normalise the multiplication sign.
|
||||
defp normalize_text(text) when is_binary(text) do
|
||||
text
|
||||
|> String.replace("″", "")
|
||||
|> String.replace("×", "x")
|
||||
end
|
||||
|
||||
defp normalize_text(text), do: text
|
||||
|
||||
defp set_credentials(api_key, store_id) do
|
||||
Process.put(:printful_api_key, api_key)
|
||||
if store_id, do: Process.put(:printful_store_id, store_id)
|
||||
|
||||
@@ -336,14 +336,16 @@ defmodule SimpleshopTheme.Providers.Printify do
|
||||
|
||||
defp normalize_product(raw) do
|
||||
options = raw["options"] || []
|
||||
raw_variants = raw["variants"] || []
|
||||
color_lookup = build_image_color_lookup(raw_variants, options)
|
||||
|
||||
%{
|
||||
provider_product_id: to_string(raw["id"]),
|
||||
title: raw["title"],
|
||||
description: raw["description"],
|
||||
category: extract_category(raw),
|
||||
images: normalize_images(raw["images"] || []),
|
||||
variants: normalize_variants(raw["variants"] || [], options),
|
||||
images: normalize_images(raw["images"] || [], color_lookup),
|
||||
variants: normalize_variants(raw_variants, options),
|
||||
provider_data: %{
|
||||
blueprint_id: raw["blueprint_id"],
|
||||
print_provider_id: raw["print_provider_id"],
|
||||
@@ -354,20 +356,51 @@ defmodule SimpleshopTheme.Providers.Printify do
|
||||
}
|
||||
end
|
||||
|
||||
defp normalize_images(images) do
|
||||
defp normalize_images(images, color_lookup) do
|
||||
images
|
||||
|> Enum.map(fn img ->
|
||||
color = resolve_image_color(img["variant_ids"] || [], color_lookup)
|
||||
%{src: img["src"], alt: img["position"], color: color, raw_position: img["position"]}
|
||||
end)
|
||||
|> select_images_per_color()
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {img, index} ->
|
||||
# Printify returns position as a label string (e.g., "front", "back")
|
||||
# We use the index as the numeric position instead
|
||||
%{
|
||||
src: img["src"],
|
||||
position: index,
|
||||
alt: img["position"]
|
||||
}
|
||||
%{src: img.src, position: index, alt: img.alt, color: img.color}
|
||||
end)
|
||||
end
|
||||
|
||||
# Hero colour (first seen) keeps all images.
|
||||
# Other colours keep only front + back views to avoid bloating the DB.
|
||||
defp select_images_per_color(images) do
|
||||
hero_color =
|
||||
images
|
||||
|> Enum.find_value(fn img -> img.color end)
|
||||
|
||||
Enum.filter(images, fn img ->
|
||||
img.color == hero_color || is_nil(img.color) ||
|
||||
img.raw_position in ["front", "back"]
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_image_color_lookup(raw_variants, options) do
|
||||
option_names = Enum.map(options, & &1["name"])
|
||||
color_index = Enum.find_index(option_names, &(&1 in ["Colors", "Color"]))
|
||||
|
||||
if color_index do
|
||||
Map.new(raw_variants, fn var ->
|
||||
parts = String.split(var["title"] || "", " / ")
|
||||
color = Enum.at(parts, color_index)
|
||||
{var["id"], color}
|
||||
end)
|
||||
else
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_image_color(variant_ids, color_lookup) do
|
||||
Enum.find_value(variant_ids, fn vid -> Map.get(color_lookup, vid) end)
|
||||
end
|
||||
|
||||
defp normalize_variants(variants, options) do
|
||||
option_names = extract_option_names(options)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user