feat: add /collections/:slug routes with category filtering

- Add ShopLive.Collection LiveView for collection pages
- Add products_by_category/1 and category_by_slug/1 to PreviewData
- Support /collections/all for all products view
- Remove /products route and ShopLive.Products (replaced by collections)
- Update all links to use /collections/all instead of /products
- Update category nav to link to /collections/:slug
- Update PDP breadcrumb to link to specific collection

URL structure now follows Shopify convention:
- /collections/all - All products
- /collections/art-prints - Art Prints collection
- /collections/apparel - Apparel collection
- etc.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 23:26:41 +00:00
parent 848c4ea146
commit 9fb836ca0d
6 changed files with 208 additions and 71 deletions

View File

@@ -0,0 +1,169 @@
defmodule SimpleshopThemeWeb.ShopLive.Collection do
use SimpleshopThemeWeb, :live_view
alias SimpleshopTheme.Settings
alias SimpleshopTheme.Media
alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator, PreviewData}
@impl true
def mount(_params, _session, socket) do
theme_settings = Settings.get_theme_settings()
generated_css =
case CSSCache.get() do
{:ok, css} -> css
:miss ->
css = CSSGenerator.generate(theme_settings)
CSSCache.put(css)
css
end
logo_image = Media.get_logo()
header_image = Media.get_header()
socket =
socket
|> assign(:theme_settings, theme_settings)
|> assign(:generated_css, generated_css)
|> assign(:logo_image, logo_image)
|> assign(:header_image, header_image)
|> assign(:mode, :shop)
|> assign(:cart_items, [])
|> assign(:cart_count, 0)
|> assign(:cart_subtotal, "£0.00")
|> assign(:categories, PreviewData.categories())
{:ok, socket}
end
@impl true
def handle_params(%{"slug" => "all"}, _uri, socket) do
{:noreply,
socket
|> assign(:page_title, "All Products")
|> assign(:collection_title, "All Products")
|> assign(:current_category, nil)
|> assign(:products, PreviewData.products())}
end
def handle_params(%{"slug" => slug}, _uri, socket) do
case PreviewData.category_by_slug(slug) do
nil ->
{:noreply,
socket
|> put_flash(:error, "Collection not found")
|> push_navigate(to: ~p"/collections/all")}
category ->
products = PreviewData.products_by_category(slug)
{:noreply,
socket
|> assign(:page_title, category.name)
|> assign(:collection_title, category.name)
|> assign(:current_category, category)
|> assign(:products, products)}
end
end
@impl true
def render(assigns) do
~H"""
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<SimpleshopThemeWeb.ShopComponents.skip_link />
<%= if @theme_settings.announcement_bar do %>
<SimpleshopThemeWeb.ShopComponents.announcement_bar theme_settings={@theme_settings} />
<% end %>
<SimpleshopThemeWeb.ShopComponents.shop_header
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
active_page="collection"
mode={@mode}
cart_count={@cart_count}
/>
<main id="main-content">
<SimpleshopThemeWeb.ShopComponents.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={@current_category && @current_category.slug} />
<SimpleshopThemeWeb.ShopComponents.product_grid theme_settings={@theme_settings}>
<%= for product <- @products do %>
<SimpleshopThemeWeb.ShopComponents.product_card
product={product}
theme_settings={@theme_settings}
mode={@mode}
variant={:default}
show_category={@current_category == nil}
/>
<% end %>
</SimpleshopThemeWeb.ShopComponents.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>
<SimpleshopThemeWeb.ShopComponents.shop_footer theme_settings={@theme_settings} mode={@mode} />
<SimpleshopThemeWeb.ShopComponents.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
<SimpleshopThemeWeb.ShopComponents.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
</div>
"""
end
defp collection_filter_bar(assigns) do
~H"""
<nav class="mb-8" aria-label="Collection filters">
<ul class="flex flex-wrap gap-2">
<li>
<.link
navigate={~p"/collections/all"}
class={[
"px-4 py-2 rounded-full text-sm 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>
<%= for category <- @categories do %>
<li>
<.link
navigate={~p"/collections/#{category.slug}"}
class={[
"px-4 py-2 rounded-full text-sm 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>
"""
end
end

View File

@@ -1,60 +0,0 @@
defmodule SimpleshopThemeWeb.ShopLive.Products do
use SimpleshopThemeWeb, :live_view
alias SimpleshopTheme.Settings
alias SimpleshopTheme.Media
alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator, PreviewData}
@impl true
def mount(_params, _session, socket) do
theme_settings = Settings.get_theme_settings()
generated_css =
case CSSCache.get() do
{:ok, css} -> css
:miss ->
css = CSSGenerator.generate(theme_settings)
CSSCache.put(css)
css
end
logo_image = Media.get_logo()
header_image = Media.get_header()
preview_data = %{
products: PreviewData.products(),
categories: PreviewData.categories()
}
socket =
socket
|> assign(:page_title, "Products")
|> assign(:theme_settings, theme_settings)
|> assign(:generated_css, generated_css)
|> assign(:logo_image, logo_image)
|> assign(:header_image, header_image)
|> assign(:preview_data, preview_data)
|> assign(:mode, :shop)
|> assign(:cart_items, [])
|> assign(:cart_count, 0)
|> assign(:cart_subtotal, "£0.00")
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<SimpleshopThemeWeb.PageTemplates.collection
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
preview_data={@preview_data}
mode={@mode}
cart_items={@cart_items}
cart_count={@cart_count}
cart_subtotal={@cart_subtotal}
/>
"""
end
end