berrypod/lib/simpleshop_theme_web/live/shop/collection.ex
jamey 35e0386abb 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>
2026-02-13 01:26:39 +00:00

229 lines
7.4 KiB
Elixir

defmodule SimpleshopThemeWeb.Shop.Collection do
use SimpleshopThemeWeb, :live_view
alias SimpleshopTheme.Theme.PreviewData
@sort_options [
{"featured", "Featured"},
{"newest", "Newest"},
{"price_asc", "Price: Low to High"},
{"price_desc", "Price: High to Low"},
{"name_asc", "Name: A-Z"},
{"name_desc", "Name: Z-A"}
]
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:categories, PreviewData.categories())
|> assign(:sort_options, @sort_options)
|> assign(:current_sort, "featured")
{:ok, socket}
end
@impl true
def handle_params(%{"slug" => slug} = params, _uri, socket) do
sort = params["sort"] || "featured"
case load_collection(slug) do
{:ok, title, category, products} ->
{:noreply,
socket
|> assign(:page_title, title)
|> assign(:collection_title, title)
|> assign(:current_category, category)
|> assign(:current_sort, sort)
|> assign(:products, sort_products(products, sort))}
:not_found ->
{:noreply,
socket
|> put_flash(:error, "Collection not found")
|> push_navigate(to: ~p"/collections/all")}
end
end
defp load_collection("all") do
{:ok, "All Products", nil, PreviewData.products()}
end
defp load_collection("sale") do
sale_products = Enum.filter(PreviewData.products(), & &1.on_sale)
{:ok, "Sale", :sale, sale_products}
end
defp load_collection(slug) do
case PreviewData.category_by_slug(slug) do
nil -> :not_found
category -> {:ok, category.name, category, PreviewData.products_by_category(slug)}
end
end
@impl true
def handle_event("sort_changed", %{"sort" => sort}, socket) do
slug =
case socket.assigns.current_category do
nil -> "all"
:sale -> "sale"
category -> category.slug
end
{:noreply, push_patch(socket, to: ~p"/collections/#{slug}?sort=#{sort}")}
end
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.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}"
defp collection_path(slug, sort), do: ~p"/collections/#{slug}?sort=#{sort}"
@impl true
def render(assigns) do
~H"""
<.shop_layout
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
mode={@mode}
cart_items={@cart_items}
cart_count={@cart_count}
cart_subtotal={@cart_subtotal}
cart_drawer_open={@cart_drawer_open}
cart_status={assigns[:cart_status]}
active_page="collection"
>
<main id="main-content">
<.collection_header
title={@collection_title}
product_count={length(@products)}
/>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<.collection_filter_bar
categories={@categories}
current_slug={
case @current_category do
:sale -> "sale"
nil -> nil
cat -> cat.slug
end
}
sort_options={@sort_options}
current_sort={@current_sort}
/>
<.product_grid theme_settings={@theme_settings}>
<%= for product <- @products do %>
<.product_card
product={product}
theme_settings={@theme_settings}
mode={@mode}
variant={:default}
show_category={@current_category in [nil, :sale]}
/>
<% end %>
</.product_grid>
<%= if @products == [] do %>
<div class="text-center py-16" style="color: var(--t-text-secondary);">
<p class="text-lg">No products found in this collection.</p>
<.link
navigate={~p"/collections/all"}
class="mt-4 inline-block underline"
style="color: var(--t-text-accent);"
>
View all products
</.link>
</div>
<% end %>
</div>
</main>
</.shop_layout>
"""
end
defp collection_filter_bar(assigns) do
~H"""
<div class="flex flex-wrap items-center justify-between gap-3 mb-6">
<nav aria-label="Collection filters" class="overflow-x-auto -mx-4 px-4 sm:mx-0 sm:px-0">
<ul class="flex gap-1.5 sm:flex-wrap sm:gap-2">
<li class="shrink-0">
<.link
navigate={collection_path("all", @current_sort)}
class={[
"px-3 py-1.5 sm:px-4 sm:py-2 rounded-full text-xs sm:text-sm whitespace-nowrap transition-colors",
if(@current_slug == nil, do: "font-medium", else: "hover:opacity-80")
]}
style={
if(@current_slug == nil,
do: "background-color: var(--t-accent); color: var(--t-text-on-accent);",
else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);"
)
}
>
All
</.link>
</li>
<li class="shrink-0">
<.link
navigate={collection_path("sale", @current_sort)}
class={[
"px-3 py-1.5 sm:px-4 sm:py-2 rounded-full text-xs sm:text-sm whitespace-nowrap transition-colors",
if(@current_slug == "sale", do: "font-medium", else: "hover:opacity-80")
]}
style={
if(@current_slug == "sale",
do: "background-color: var(--t-accent); color: var(--t-text-on-accent);",
else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);"
)
}
>
Sale
</.link>
</li>
<%= for category <- @categories do %>
<li class="shrink-0">
<.link
navigate={collection_path(category.slug, @current_sort)}
class={[
"px-3 py-1.5 sm:px-4 sm:py-2 rounded-full text-xs sm:text-sm whitespace-nowrap transition-colors",
if(@current_slug == category.slug, do: "font-medium", else: "hover:opacity-80")
]}
style={
if(@current_slug == category.slug,
do: "background-color: var(--t-accent); color: var(--t-text-on-accent);",
else: "background-color: var(--t-surface-raised); color: var(--t-text-primary);"
)
}
>
{category.name}
</.link>
</li>
<% end %>
</ul>
</nav>
<form phx-change="sort_changed">
<.shop_select
name="sort"
options={@sort_options}
selected={@current_sort}
class="px-3 py-1.5 sm:px-4 sm:py-2 text-xs sm:text-sm"
aria-label="Sort products"
/>
</form>
</div>
"""
end
end