feat: add dynamic variant selector with color swatches
- Fix Printify options parsing (Color/Size were swapped) - Add extract_option_types/1 for frontend display with hex colors - Filter option types to only published variants (not full catalog) - Track selected variant in LiveView with price updates - Color swatches for color-type options, text buttons for size - Disable unavailable combinations - Add startup recovery for stale sync status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -236,6 +236,27 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
{image_url, image_id, source_width} = image_attrs(first_image)
|
||||
{hover_image_url, hover_image_id, hover_source_width} = image_attrs(second_image)
|
||||
|
||||
# Map variants to frontend format (only enabled/published ones)
|
||||
variants =
|
||||
product.variants
|
||||
|> Enum.filter(& &1.is_enabled)
|
||||
|> Enum.map(fn v ->
|
||||
%{
|
||||
id: v.id,
|
||||
provider_variant_id: v.provider_variant_id,
|
||||
title: v.title,
|
||||
price: v.price,
|
||||
compare_at_price: v.compare_at_price,
|
||||
options: v.options,
|
||||
is_available: v.is_available
|
||||
}
|
||||
end)
|
||||
|
||||
# Extract option types, filtered to only values present in enabled variants
|
||||
option_types =
|
||||
SimpleshopTheme.Providers.Printify.extract_option_types(product.provider_data)
|
||||
|> filter_option_types_by_variants(variants)
|
||||
|
||||
%{
|
||||
id: product.slug,
|
||||
name: product.title,
|
||||
@@ -252,7 +273,9 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
slug: product.slug,
|
||||
in_stock: in_stock,
|
||||
on_sale: on_sale,
|
||||
inserted_at: product.inserted_at
|
||||
inserted_at: product.inserted_at,
|
||||
option_types: option_types,
|
||||
variants: variants
|
||||
}
|
||||
end
|
||||
|
||||
@@ -270,6 +293,31 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
{src, nil, nil}
|
||||
end
|
||||
|
||||
# Filter option types to only include values present in enabled variants
|
||||
# This ensures we don't show unpublished options from the Printify catalog
|
||||
defp filter_option_types_by_variants(option_types, variants) do
|
||||
# Collect all option values present in the enabled variants
|
||||
values_in_use =
|
||||
Enum.reduce(variants, %{}, fn variant, acc ->
|
||||
Enum.reduce(variant.options, acc, fn {opt_name, opt_value}, inner_acc ->
|
||||
Map.update(inner_acc, opt_name, MapSet.new([opt_value]), &MapSet.put(&1, opt_value))
|
||||
end)
|
||||
end)
|
||||
|
||||
# Filter each option type's values to only those in use
|
||||
option_types
|
||||
|> Enum.map(fn opt_type ->
|
||||
used_values = Map.get(values_in_use, opt_type.name, MapSet.new())
|
||||
|
||||
filtered_values =
|
||||
opt_type.values
|
||||
|> Enum.filter(fn v -> MapSet.member?(used_values, v.title) end)
|
||||
|
||||
%{opt_type | values: filtered_values}
|
||||
end)
|
||||
|> Enum.reject(fn opt_type -> opt_type.values == [] end)
|
||||
end
|
||||
|
||||
# Default source width for mockup variants (max generated size)
|
||||
@mockup_source_width 1200
|
||||
|
||||
@@ -280,7 +328,7 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
id: "1",
|
||||
name: "Mountain Sunrise Art Print",
|
||||
description: "Capture the magic of dawn with this stunning mountain landscape print",
|
||||
price: 2400,
|
||||
price: 1999,
|
||||
compare_at_price: nil,
|
||||
image_url: "/mockups/mountain-sunrise-print-1",
|
||||
hover_image_url: "/mockups/mountain-sunrise-print-2",
|
||||
@@ -288,7 +336,41 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
hover_source_width: @mockup_source_width,
|
||||
category: "Art Prints",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
on_sale: false,
|
||||
option_types: [
|
||||
%{
|
||||
name: "Size",
|
||||
type: :size,
|
||||
values: [
|
||||
%{id: 1, title: "8×10"},
|
||||
%{id: 2, title: "12×18"},
|
||||
%{id: 3, title: "18×24"}
|
||||
]
|
||||
}
|
||||
],
|
||||
variants: [
|
||||
%{
|
||||
id: "p1",
|
||||
title: "8×10",
|
||||
price: 1999,
|
||||
options: %{"Size" => "8×10"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "p2",
|
||||
title: "12×18",
|
||||
price: 2400,
|
||||
options: %{"Size" => "12×18"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "p3",
|
||||
title: "18×24",
|
||||
price: 3200,
|
||||
options: %{"Size" => "18×24"},
|
||||
is_available: true
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
id: "2",
|
||||
@@ -359,7 +441,176 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
hover_source_width: @mockup_source_width,
|
||||
category: "Apparel",
|
||||
in_stock: true,
|
||||
on_sale: false
|
||||
on_sale: false,
|
||||
option_types: [
|
||||
%{
|
||||
name: "Color",
|
||||
type: :color,
|
||||
values: [
|
||||
%{id: 1, title: "Black", hex: "#000000"},
|
||||
%{id: 2, title: "Navy", hex: "#1A2237"},
|
||||
%{id: 3, title: "White", hex: "#FFFFFF"},
|
||||
%{id: 4, title: "Sport Grey", hex: "#9CA3AF"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
name: "Size",
|
||||
type: :size,
|
||||
values: [
|
||||
%{id: 10, title: "S"},
|
||||
%{id: 11, title: "M"},
|
||||
%{id: 12, title: "L"},
|
||||
%{id: 13, title: "XL"},
|
||||
%{id: 14, title: "2XL"}
|
||||
]
|
||||
}
|
||||
],
|
||||
variants: [
|
||||
# Black variants
|
||||
%{
|
||||
id: "t1",
|
||||
title: "Black / S",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Black", "Size" => "S"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t2",
|
||||
title: "Black / M",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Black", "Size" => "M"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t3",
|
||||
title: "Black / L",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Black", "Size" => "L"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t4",
|
||||
title: "Black / XL",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Black", "Size" => "XL"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t5",
|
||||
title: "Black / 2XL",
|
||||
price: 3299,
|
||||
options: %{"Color" => "Black", "Size" => "2XL"},
|
||||
is_available: true
|
||||
},
|
||||
# Navy variants
|
||||
%{
|
||||
id: "t6",
|
||||
title: "Navy / S",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Navy", "Size" => "S"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t7",
|
||||
title: "Navy / M",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Navy", "Size" => "M"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t8",
|
||||
title: "Navy / L",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Navy", "Size" => "L"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t9",
|
||||
title: "Navy / XL",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Navy", "Size" => "XL"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t10",
|
||||
title: "Navy / 2XL",
|
||||
price: 3299,
|
||||
options: %{"Color" => "Navy", "Size" => "2XL"},
|
||||
is_available: false
|
||||
},
|
||||
# White variants
|
||||
%{
|
||||
id: "t11",
|
||||
title: "White / S",
|
||||
price: 2999,
|
||||
options: %{"Color" => "White", "Size" => "S"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t12",
|
||||
title: "White / M",
|
||||
price: 2999,
|
||||
options: %{"Color" => "White", "Size" => "M"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t13",
|
||||
title: "White / L",
|
||||
price: 2999,
|
||||
options: %{"Color" => "White", "Size" => "L"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t14",
|
||||
title: "White / XL",
|
||||
price: 2999,
|
||||
options: %{"Color" => "White", "Size" => "XL"},
|
||||
is_available: false
|
||||
},
|
||||
%{
|
||||
id: "t15",
|
||||
title: "White / 2XL",
|
||||
price: 3299,
|
||||
options: %{"Color" => "White", "Size" => "2XL"},
|
||||
is_available: false
|
||||
},
|
||||
# Sport Grey variants
|
||||
%{
|
||||
id: "t16",
|
||||
title: "Sport Grey / S",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Sport Grey", "Size" => "S"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t17",
|
||||
title: "Sport Grey / M",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Sport Grey", "Size" => "M"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t18",
|
||||
title: "Sport Grey / L",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Sport Grey", "Size" => "L"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t19",
|
||||
title: "Sport Grey / XL",
|
||||
price: 2999,
|
||||
options: %{"Color" => "Sport Grey", "Size" => "XL"},
|
||||
is_available: true
|
||||
},
|
||||
%{
|
||||
id: "t20",
|
||||
title: "Sport Grey / 2XL",
|
||||
price: 3299,
|
||||
options: %{"Color" => "Sport Grey", "Size" => "2XL"},
|
||||
is_available: true
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
id: "7",
|
||||
|
||||
Reference in New Issue
Block a user