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:
parent
848c4ea146
commit
9fb836ca0d
@ -114,6 +114,34 @@ defmodule SimpleshopTheme.Theme.PreviewData do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a category by its slug.
|
||||
|
||||
Returns nil if not found.
|
||||
"""
|
||||
def category_by_slug(slug) do
|
||||
Enum.find(categories(), fn cat -> cat.slug == slug end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns products filtered by category slug.
|
||||
|
||||
If slug is nil or "all", returns all products.
|
||||
"""
|
||||
def products_by_category(nil), do: products()
|
||||
def products_by_category("all"), do: products()
|
||||
|
||||
def products_by_category(slug) do
|
||||
case category_by_slug(slug) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
category ->
|
||||
products()
|
||||
|> Enum.filter(fn product -> product.category == category.name end)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the shop has real products.
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<.breadcrumb items={[
|
||||
%{label: "Home", page: "home", href: "/"},
|
||||
%{label: @product.category, page: "collection", href: "/products"},
|
||||
%{label: @product.category, page: "collection", href: "/collections/#{@product.category |> String.downcase() |> String.replace(" ", "-")}"},
|
||||
%{label: @product.name, current: true}
|
||||
]} mode={@mode} />
|
||||
|
||||
|
||||
@ -184,9 +184,9 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
<li><a href="#" phx-click="change_preview_page" phx-value-page="collection" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary); cursor: pointer;">New arrivals</a></li>
|
||||
<li><a href="#" phx-click="change_preview_page" phx-value-page="collection" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary); cursor: pointer;">Best sellers</a></li>
|
||||
<% else %>
|
||||
<li><a href="/products" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">All products</a></li>
|
||||
<li><a href="/products?sort=newest" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">New arrivals</a></li>
|
||||
<li><a href="/products?sort=popular" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">Best sellers</a></li>
|
||||
<li><a href="/collections/all" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">All products</a></li>
|
||||
<li><a href="/collections/all" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">New arrivals</a></li>
|
||||
<li><a href="/collections/all" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">Best sellers</a></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
@ -327,7 +327,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
<a href="#" phx-click="change_preview_page" phx-value-page="contact" aria-current={if @active_page == "contact", do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">Contact</a>
|
||||
<% else %>
|
||||
<a href="/" aria-current={if @active_page == "home", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
|
||||
<a href="/products" aria-current={if @active_page in ["collection", "pdp"], do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
|
||||
<a href="/collections/all" aria-current={if @active_page in ["collection", "pdp"], do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Shop</a>
|
||||
<a href="/about" aria-current={if @active_page == "about", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">About</a>
|
||||
<a href="/contact" aria-current={if @active_page == "contact", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
|
||||
<% end %>
|
||||
@ -1078,7 +1078,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
</span>
|
||||
</a>
|
||||
<% else %>
|
||||
<a href={"/products?category=#{category[:slug] || category.name}"} class="flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5" style="text-decoration: none;">
|
||||
<a href={"/collections/#{category.slug}"} class="flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5" style="text-decoration: none;">
|
||||
<div
|
||||
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105"
|
||||
style={"background-image: url('#{category.image_url}');"}
|
||||
@ -1106,7 +1106,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
* `mode` - Either `:live` (default) or `:preview`.
|
||||
* `cta_text` - Optional. Text for the "view all" button. Defaults to "View all products".
|
||||
* `cta_page` - Optional. Page to navigate to (preview mode). Defaults to "collection".
|
||||
* `cta_href` - Optional. URL for live mode. Defaults to "/products".
|
||||
* `cta_href` - Optional. URL for live mode. Defaults to "/collections/all".
|
||||
|
||||
## Examples
|
||||
|
||||
@ -1124,7 +1124,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
attr :mode, :atom, default: :live
|
||||
attr :cta_text, :string, default: "View all products"
|
||||
attr :cta_page, :string, default: "collection"
|
||||
attr :cta_href, :string, default: "/products"
|
||||
attr :cta_href, :string, default: "/collections/all"
|
||||
|
||||
def featured_products_section(assigns) do
|
||||
~H"""
|
||||
@ -1785,7 +1785,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
</button>
|
||||
<% else %>
|
||||
<a
|
||||
href="/products"
|
||||
href="/collections/all"
|
||||
class="block w-full px-6 py-3 font-semibold transition-all text-center"
|
||||
style="border: 2px solid var(--t-border-default); color: var(--t-text-primary); border-radius: var(--t-radius-button); text-decoration: none;"
|
||||
>
|
||||
@ -1812,7 +1812,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
|
||||
<.breadcrumb items={[
|
||||
%{label: "Home", page: "home", href: "/"},
|
||||
%{label: "Art Prints", page: "collection", href: "/products?category=art-prints"},
|
||||
%{label: "Art Prints", page: "collection", href: "/collections/art-prints"},
|
||||
%{label: "Mountain Sunrise", current: true}
|
||||
]} mode={:preview} />
|
||||
"""
|
||||
|
||||
169
lib/simpleshop_theme_web/live/shop_live/collection.ex
Normal file
169
lib/simpleshop_theme_web/live/shop_live/collection.ex
Normal 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
|
||||
@ -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
|
||||
@ -29,7 +29,7 @@ defmodule SimpleshopThemeWeb.Router do
|
||||
live "/", ShopLive.Home, :index
|
||||
live "/about", ShopLive.About, :index
|
||||
live "/contact", ShopLive.Contact, :index
|
||||
live "/products", ShopLive.Products, :index
|
||||
live "/collections/:slug", ShopLive.Collection, :show
|
||||
live "/products/:id", ShopLive.ProductShow, :show
|
||||
live "/cart", ShopLive.Cart, :index
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user