add denormalized product fields and use Product structs throughout

Adds cheapest_price, compare_at_price, in_stock, on_sale columns to
products table (recomputed from variants after each sync). Shop
components now work with Product structs directly instead of plain
maps from PreviewData. Renames .name to .title, adds Product display
helpers (primary_image, hover_image, option_types) and ProductImage
helpers (display_url, direct_url, source_width). Adds Products context
query functions for storefront use (list_visible_products,
get_visible_product, list_categories with DB-level sort/filter).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-13 01:26:39 +00:00
parent 0b4fe031b7
commit 35e0386abb
20 changed files with 1000 additions and 328 deletions

View File

@@ -353,7 +353,7 @@ defmodule SimpleshopThemeWeb.Admin.Theme.Index do
end)
display_price =
if selected_variant, do: selected_variant.price, else: product.price
if selected_variant, do: selected_variant.price, else: product.cheapest_price
assigns =
assigns
@@ -374,7 +374,9 @@ defmodule SimpleshopThemeWeb.Admin.Theme.Index do
cart_items = assigns.preview_data.cart_items
subtotal =
Enum.reduce(cart_items, 0, fn item, acc -> acc + item.product.price * item.quantity end)
Enum.reduce(cart_items, 0, fn item, acc ->
acc + item.product.cheapest_price * item.quantity
end)
assigns =
assigns
@@ -464,6 +466,15 @@ defmodule SimpleshopThemeWeb.Admin.Theme.Index do
end
defp build_gallery_images(product) do
[product.image_url, product.hover_image_url, product.image_url, product.hover_image_url]
alias SimpleshopTheme.Products.ProductImage
(product[:images] || [])
|> Enum.sort_by(& &1.position)
|> Enum.map(fn img -> ProductImage.direct_url(img, 1200) end)
|> Enum.reject(&is_nil/1)
|> case do
[] -> []
urls -> urls
end
end
end

View File

@@ -75,10 +75,13 @@ defmodule SimpleshopThemeWeb.Shop.Collection do
defp sort_products(products, "featured"), do: products
defp sort_products(products, "newest"), do: Enum.reverse(products)
defp sort_products(products, "price_asc"), do: Enum.sort_by(products, & &1.price)
defp sort_products(products, "price_desc"), do: Enum.sort_by(products, & &1.price, :desc)
defp sort_products(products, "name_asc"), do: Enum.sort_by(products, & &1.name)
defp sort_products(products, "name_desc"), do: Enum.sort_by(products, & &1.name, :desc)
defp sort_products(products, "price_asc"), do: Enum.sort_by(products, & &1.cheapest_price)
defp sort_products(products, "price_desc"),
do: Enum.sort_by(products, & &1.cheapest_price, :desc)
defp sort_products(products, "name_asc"), do: Enum.sort_by(products, & &1.title)
defp sort_products(products, "name_desc"), do: Enum.sort_by(products, & &1.title, :desc)
defp sort_products(products, _), do: products
defp collection_path(slug, "featured"), do: ~p"/collections/#{slug}"

View File

@@ -2,6 +2,7 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
use SimpleshopThemeWeb, :live_view
alias SimpleshopTheme.Cart
alias SimpleshopTheme.Products.{Product, ProductImage}
alias SimpleshopTheme.Theme.PreviewData
@impl true
@@ -19,14 +20,13 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
# Build gallery images from local image_id or external URL
gallery_images =
[
image_src(product[:image_id], product[:image_url]),
image_src(product[:hover_image_id], product[:hover_image_url])
]
(product[:images] || [])
|> Enum.sort_by(& &1.position)
|> Enum.map(fn img -> ProductImage.direct_url(img, 1200) end)
|> Enum.reject(&is_nil/1)
# Initialize variant selection
option_types = product[:option_types] || []
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)
@@ -34,7 +34,7 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
socket =
socket
|> assign(:page_title, product.name)
|> assign(:page_title, product.title)
|> assign(:product, product)
|> assign(:gallery_images, gallery_images)
|> assign(:related_products, related_products)
@@ -56,16 +56,6 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
List.first(products)
end
# Build image source URL - prefer local image_id, fall back to external URL
defp image_src(image_id, _url) when is_binary(image_id) do
"/images/#{image_id}/variant/1200.webp"
end
# Mock data uses base paths like "/mockups/product-1" — append size + format
defp image_src(_, "/mockups/" <> _ = url), do: "#{url}-1200.webp"
defp image_src(_, url) when is_binary(url), do: url
defp image_src(_, _), do: nil
# Select first available variant by default
defp initialize_variant_selection([first | _] = _variants) do
{first.options, first}
@@ -98,7 +88,7 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
end
defp variant_price(%{price: price}, _product) when is_integer(price), do: price
defp variant_price(_, %{price: price}), do: price
defp variant_price(_, %{cheapest_price: price}), do: price
defp variant_price(_, _), do: 0
defp find_variant(variants, selected_options) do
@@ -154,7 +144,7 @@ defmodule SimpleshopThemeWeb.Shop.ProductShow do
|> SimpleshopThemeWeb.CartHook.broadcast_and_update(cart)
|> assign(:quantity, 1)
|> assign(:cart_drawer_open, true)
|> assign(:cart_status, "#{socket.assigns.product.name} added to cart")
|> assign(:cart_status, "#{socket.assigns.product.title} added to cart")
{:noreply, socket}
else