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
|
||||||
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 """
|
@doc """
|
||||||
Checks if the shop has real products.
|
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">
|
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<.breadcrumb items={[
|
<.breadcrumb items={[
|
||||||
%{label: "Home", page: "home", href: "/"},
|
%{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}
|
%{label: @product.name, current: true}
|
||||||
]} mode={@mode} />
|
]} 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;">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>
|
<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 %>
|
<% else %>
|
||||||
<li><a href="/products" 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);">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="/collections/all" 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);">Best sellers</a></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
<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 %>
|
<% else %>
|
||||||
<a href="/" aria-current={if @active_page == "home", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Home</a>
|
<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="/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>
|
<a href="/contact" aria-current={if @active_page == "contact", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Contact</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -1078,7 +1078,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<% else %>
|
<% 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
|
<div
|
||||||
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105"
|
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}');"}
|
style={"background-image: url('#{category.image_url}');"}
|
||||||
@ -1106,7 +1106,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
|||||||
* `mode` - Either `:live` (default) or `:preview`.
|
* `mode` - Either `:live` (default) or `:preview`.
|
||||||
* `cta_text` - Optional. Text for the "view all" button. Defaults to "View all products".
|
* `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_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
|
## Examples
|
||||||
|
|
||||||
@ -1124,7 +1124,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
|||||||
attr :mode, :atom, default: :live
|
attr :mode, :atom, default: :live
|
||||||
attr :cta_text, :string, default: "View all products"
|
attr :cta_text, :string, default: "View all products"
|
||||||
attr :cta_page, :string, default: "collection"
|
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
|
def featured_products_section(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@ -1785,7 +1785,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
|||||||
</button>
|
</button>
|
||||||
<% else %>
|
<% else %>
|
||||||
<a
|
<a
|
||||||
href="/products"
|
href="/collections/all"
|
||||||
class="block w-full px-6 py-3 font-semibold transition-all text-center"
|
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;"
|
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={[
|
<.breadcrumb items={[
|
||||||
%{label: "Home", page: "home", href: "/"},
|
%{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}
|
%{label: "Mountain Sunrise", current: true}
|
||||||
]} mode={:preview} />
|
]} 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 "/", ShopLive.Home, :index
|
||||||
live "/about", ShopLive.About, :index
|
live "/about", ShopLive.About, :index
|
||||||
live "/contact", ShopLive.Contact, :index
|
live "/contact", ShopLive.Contact, :index
|
||||||
live "/products", ShopLive.Products, :index
|
live "/collections/:slug", ShopLive.Collection, :show
|
||||||
live "/products/:id", ShopLive.ProductShow, :show
|
live "/products/:id", ShopLive.ProductShow, :show
|
||||||
live "/cart", ShopLive.Cart, :index
|
live "/cart", ShopLive.Cart, :index
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user