2026-02-12 00:16:32 +00:00
|
|
|
defmodule SimpleshopThemeWeb.Shop.Collection do
|
2026-01-19 23:26:41 +00:00
|
|
|
use SimpleshopThemeWeb, :live_view
|
|
|
|
|
|
2026-02-08 11:59:33 +00:00
|
|
|
alias SimpleshopTheme.Theme.PreviewData
|
2026-01-19 23:26:41 +00:00
|
|
|
|
2026-01-19 23:38:22 +00:00
|
|
|
@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"}
|
|
|
|
|
]
|
|
|
|
|
|
2026-01-19 23:26:41 +00:00
|
|
|
@impl true
|
|
|
|
|
def mount(_params, _session, socket) do
|
|
|
|
|
socket =
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:categories, PreviewData.categories())
|
2026-01-19 23:38:22 +00:00
|
|
|
|> assign(:sort_options, @sort_options)
|
|
|
|
|
|> assign(:current_sort, "featured")
|
2026-01-19 23:26:41 +00:00
|
|
|
|
|
|
|
|
{:ok, socket}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@impl true
|
2026-01-19 23:38:22 +00:00
|
|
|
def handle_params(%{"slug" => slug} = params, _uri, socket) do
|
|
|
|
|
sort = params["sort"] || "featured"
|
2026-01-19 23:26:41 +00:00
|
|
|
|
2026-01-19 23:38:22 +00:00
|
|
|
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 ->
|
2026-01-19 23:26:41 +00:00
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> put_flash(:error, "Collection not found")
|
|
|
|
|
|> push_navigate(to: ~p"/collections/all")}
|
2026-01-19 23:38:22 +00:00
|
|
|
end
|
|
|
|
|
end
|
2026-01-19 23:26:41 +00:00
|
|
|
|
2026-01-19 23:38:22 +00:00
|
|
|
defp load_collection("all") do
|
|
|
|
|
{:ok, "All Products", nil, PreviewData.products()}
|
|
|
|
|
end
|
2026-01-19 23:26:41 +00:00
|
|
|
|
2026-02-11 08:38:54 +00:00
|
|
|
defp load_collection("sale") do
|
|
|
|
|
sale_products = Enum.filter(PreviewData.products(), & &1.on_sale)
|
|
|
|
|
{:ok, "Sale", :sale, sale_products}
|
|
|
|
|
end
|
|
|
|
|
|
2026-01-19 23:38:22 +00:00
|
|
|
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)}
|
2026-01-19 23:26:41 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2026-01-19 23:38:22 +00:00
|
|
|
@impl true
|
|
|
|
|
def handle_event("sort_changed", %{"sort" => sort}, socket) do
|
2026-01-31 14:24:58 +00:00
|
|
|
slug =
|
2026-02-11 08:38:54 +00:00
|
|
|
case socket.assigns.current_category do
|
|
|
|
|
nil -> "all"
|
|
|
|
|
:sale -> "sale"
|
|
|
|
|
category -> category.slug
|
|
|
|
|
end
|
2026-01-31 14:24:58 +00:00
|
|
|
|
2026-01-19 23:38:22 +00:00
|
|
|
{: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)
|
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
|
|
|
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)
|
2026-01-19 23:38:22 +00:00
|
|
|
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}"
|
|
|
|
|
|
2026-01-19 23:26:41 +00:00
|
|
|
@impl true
|
|
|
|
|
def render(assigns) do
|
|
|
|
|
~H"""
|
refactor: split shop_components.ex into 5 focused sub-modules
4,487-line monolith → 23-line facade + 5 modules:
- Base (inputs, buttons, cards)
- Layout (header, footer, mobile nav, shop_layout)
- Cart (drawer, items, order summary)
- Product (cards, gallery, variant selector, hero)
- Content (rich text, images, contact, reviews)
`use SimpleshopThemeWeb.ShopComponents` imports all sub-modules.
No single file over ~1,600 lines now.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:30:25 +00:00
|
|
|
<.shop_layout
|
2026-02-08 12:10:08 +00:00
|
|
|
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"
|
2026-01-31 14:24:58 +00:00
|
|
|
>
|
2026-01-19 23:26:41 +00:00
|
|
|
<main id="main-content">
|
refactor: split shop_components.ex into 5 focused sub-modules
4,487-line monolith → 23-line facade + 5 modules:
- Base (inputs, buttons, cards)
- Layout (header, footer, mobile nav, shop_layout)
- Cart (drawer, items, order summary)
- Product (cards, gallery, variant selector, hero)
- Content (rich text, images, contact, reviews)
`use SimpleshopThemeWeb.ShopComponents` imports all sub-modules.
No single file over ~1,600 lines now.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:30:25 +00:00
|
|
|
<.collection_header
|
2026-01-19 23:26:41 +00:00
|
|
|
title={@collection_title}
|
|
|
|
|
product_count={length(@products)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
2026-01-19 23:38:22 +00:00
|
|
|
<.collection_filter_bar
|
|
|
|
|
categories={@categories}
|
2026-02-11 08:38:54 +00:00
|
|
|
current_slug={
|
|
|
|
|
case @current_category do
|
|
|
|
|
:sale -> "sale"
|
|
|
|
|
nil -> nil
|
|
|
|
|
cat -> cat.slug
|
|
|
|
|
end
|
|
|
|
|
}
|
2026-01-19 23:38:22 +00:00
|
|
|
sort_options={@sort_options}
|
|
|
|
|
current_sort={@current_sort}
|
|
|
|
|
/>
|
2026-01-19 23:26:41 +00:00
|
|
|
|
refactor: split shop_components.ex into 5 focused sub-modules
4,487-line monolith → 23-line facade + 5 modules:
- Base (inputs, buttons, cards)
- Layout (header, footer, mobile nav, shop_layout)
- Cart (drawer, items, order summary)
- Product (cards, gallery, variant selector, hero)
- Content (rich text, images, contact, reviews)
`use SimpleshopThemeWeb.ShopComponents` imports all sub-modules.
No single file over ~1,600 lines now.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:30:25 +00:00
|
|
|
<.product_grid theme_settings={@theme_settings}>
|
2026-01-19 23:26:41 +00:00
|
|
|
<%= for product <- @products do %>
|
refactor: split shop_components.ex into 5 focused sub-modules
4,487-line monolith → 23-line facade + 5 modules:
- Base (inputs, buttons, cards)
- Layout (header, footer, mobile nav, shop_layout)
- Cart (drawer, items, order summary)
- Product (cards, gallery, variant selector, hero)
- Content (rich text, images, contact, reviews)
`use SimpleshopThemeWeb.ShopComponents` imports all sub-modules.
No single file over ~1,600 lines now.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:30:25 +00:00
|
|
|
<.product_card
|
2026-01-19 23:26:41 +00:00
|
|
|
product={product}
|
|
|
|
|
theme_settings={@theme_settings}
|
|
|
|
|
mode={@mode}
|
|
|
|
|
variant={:default}
|
2026-02-11 08:38:54 +00:00
|
|
|
show_category={@current_category in [nil, :sale]}
|
2026-01-19 23:26:41 +00:00
|
|
|
/>
|
|
|
|
|
<% end %>
|
refactor: split shop_components.ex into 5 focused sub-modules
4,487-line monolith → 23-line facade + 5 modules:
- Base (inputs, buttons, cards)
- Layout (header, footer, mobile nav, shop_layout)
- Cart (drawer, items, order summary)
- Product (cards, gallery, variant selector, hero)
- Content (rich text, images, contact, reviews)
`use SimpleshopThemeWeb.ShopComponents` imports all sub-modules.
No single file over ~1,600 lines now.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:30:25 +00:00
|
|
|
</.product_grid>
|
2026-01-19 23:26:41 +00:00
|
|
|
|
|
|
|
|
<%= 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>
|
2026-01-31 14:24:58 +00:00
|
|
|
<.link
|
|
|
|
|
navigate={~p"/collections/all"}
|
|
|
|
|
class="mt-4 inline-block underline"
|
|
|
|
|
style="color: var(--t-text-accent);"
|
|
|
|
|
>
|
2026-01-19 23:26:41 +00:00
|
|
|
View all products
|
|
|
|
|
</.link>
|
|
|
|
|
</div>
|
|
|
|
|
<% end %>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
refactor: split shop_components.ex into 5 focused sub-modules
4,487-line monolith → 23-line facade + 5 modules:
- Base (inputs, buttons, cards)
- Layout (header, footer, mobile nav, shop_layout)
- Cart (drawer, items, order summary)
- Product (cards, gallery, variant selector, hero)
- Content (rich text, images, contact, reviews)
`use SimpleshopThemeWeb.ShopComponents` imports all sub-modules.
No single file over ~1,600 lines now.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:30:25 +00:00
|
|
|
</.shop_layout>
|
2026-01-19 23:26:41 +00:00
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp collection_filter_bar(assigns) do
|
|
|
|
|
~H"""
|
2026-02-11 08:38:54 +00:00
|
|
|
<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">
|
2026-01-19 23:26:41 +00:00
|
|
|
<.link
|
2026-01-19 23:38:22 +00:00
|
|
|
navigate={collection_path("all", @current_sort)}
|
2026-01-19 23:26:41 +00:00
|
|
|
class={[
|
2026-02-11 08:38:54 +00:00
|
|
|
"px-3 py-1.5 sm:px-4 sm:py-2 rounded-full text-xs sm:text-sm whitespace-nowrap transition-colors",
|
2026-01-19 23:38:22 +00:00
|
|
|
if(@current_slug == nil, do: "font-medium", else: "hover:opacity-80")
|
2026-01-19 23:26:41 +00:00
|
|
|
]}
|
2026-01-31 14:24:58 +00:00
|
|
|
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);"
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-01-19 23:26:41 +00:00
|
|
|
>
|
2026-01-19 23:38:22 +00:00
|
|
|
All
|
2026-01-19 23:26:41 +00:00
|
|
|
</.link>
|
|
|
|
|
</li>
|
2026-02-11 08:38:54 +00:00
|
|
|
<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>
|
2026-01-19 23:38:22 +00:00
|
|
|
<%= for category <- @categories do %>
|
2026-02-11 08:38:54 +00:00
|
|
|
<li class="shrink-0">
|
2026-01-19 23:38:22 +00:00
|
|
|
<.link
|
|
|
|
|
navigate={collection_path(category.slug, @current_sort)}
|
|
|
|
|
class={[
|
2026-02-11 08:38:54 +00:00
|
|
|
"px-3 py-1.5 sm:px-4 sm:py-2 rounded-full text-xs sm:text-sm whitespace-nowrap transition-colors",
|
2026-01-19 23:38:22 +00:00
|
|
|
if(@current_slug == category.slug, do: "font-medium", else: "hover:opacity-80")
|
|
|
|
|
]}
|
2026-01-31 14:24:58 +00:00
|
|
|
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);"
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-01-19 23:38:22 +00:00
|
|
|
>
|
|
|
|
|
{category.name}
|
|
|
|
|
</.link>
|
|
|
|
|
</li>
|
|
|
|
|
<% end %>
|
|
|
|
|
</ul>
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<form phx-change="sort_changed">
|
2026-01-25 19:09:49 +00:00
|
|
|
<.shop_select
|
2026-01-19 23:38:22 +00:00
|
|
|
name="sort"
|
2026-01-25 19:09:49 +00:00
|
|
|
options={@sort_options}
|
|
|
|
|
selected={@current_sort}
|
2026-02-11 08:38:54 +00:00
|
|
|
class="px-3 py-1.5 sm:px-4 sm:py-2 text-xs sm:text-sm"
|
2026-01-19 23:38:22 +00:00
|
|
|
aria-label="Sort products"
|
2026-01-25 19:09:49 +00:00
|
|
|
/>
|
2026-01-19 23:38:22 +00:00
|
|
|
</form>
|
|
|
|
|
</div>
|
2026-01-19 23:26:41 +00:00
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
end
|