wire shop LiveViews to DB queries and improve search UX
Replace PreviewData indirection in all shop LiveViews with direct Products context queries. Home, collection, product detail and error pages now query the database. Categories loaded once in ThemeHook. Cart hydration no longer falls back to mock data. PreviewData kept only for the theme editor. Search modal gains keyboard navigation (arrow keys, Enter, Escape), Cmd+K/Ctrl+K shortcut, full ARIA combobox pattern, LiveView navigate links, and 150ms debounce. SearchModal JS hook manages selection state and highlight. search.ex gets transaction safety on reindex and a public remove_product/1. 10 new integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,13 +13,13 @@
|
||||
search_results={assigns[:search_results] || []}
|
||||
>
|
||||
<main id="main-content">
|
||||
<.collection_header title="All Products" product_count={length(@preview_data.products)} />
|
||||
<.collection_header title="All Products" product_count={length(assigns[:products] || [])} />
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<.filter_bar categories={@preview_data.categories} />
|
||||
<.filter_bar categories={assigns[:categories] || []} />
|
||||
|
||||
<.product_grid theme_settings={@theme_settings}>
|
||||
<%= for product <- @preview_data.products do %>
|
||||
<%= for product <- assigns[:products] || [] do %>
|
||||
<.product_card
|
||||
product={product}
|
||||
theme_settings={@theme_settings}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
/>
|
||||
|
||||
<.product_grid columns={:fixed_4} gap="gap-4" class="mt-12 max-w-xl mx-auto">
|
||||
<%= for product <- Enum.take(@preview_data.products, 4) do %>
|
||||
<%= for product <- Enum.take(assigns[:products] || [], 4) do %>
|
||||
<.product_card
|
||||
product={product}
|
||||
theme_settings={@theme_settings}
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
mode={@mode}
|
||||
/>
|
||||
|
||||
<.category_nav categories={@preview_data.categories} mode={@mode} />
|
||||
<.category_nav categories={assigns[:categories] || []} mode={@mode} />
|
||||
|
||||
<.featured_products_section
|
||||
title="Featured products"
|
||||
products={@preview_data.products}
|
||||
products={assigns[:products] || []}
|
||||
theme_settings={@theme_settings}
|
||||
mode={@mode}
|
||||
/>
|
||||
|
||||
@@ -97,7 +97,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
|
||||
|
||||
{render_slot(@inner_block)}
|
||||
|
||||
<.shop_footer theme_settings={@theme_settings} mode={@mode} />
|
||||
<.shop_footer
|
||||
theme_settings={@theme_settings}
|
||||
mode={@mode}
|
||||
categories={assigns[:categories] || []}
|
||||
/>
|
||||
|
||||
<.cart_drawer
|
||||
cart_items={@cart_items}
|
||||
@@ -341,9 +345,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
|
||||
assign(
|
||||
assigns,
|
||||
:results_with_images,
|
||||
Enum.map(assigns.search_results, fn product ->
|
||||
assigns.search_results
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {product, idx} ->
|
||||
image = Product.primary_image(product)
|
||||
%{product: product, image_url: ProductImage.direct_url(image, 96)}
|
||||
%{product: product, image_url: ProductImage.direct_url(image, 400), idx: idx}
|
||||
end)
|
||||
)
|
||||
|
||||
@@ -352,6 +358,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
|
||||
id="search-modal"
|
||||
class="search-modal"
|
||||
style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1001; display: none; align-items: flex-start; justify-content: center; padding-top: 10vh;"
|
||||
phx-hook="SearchModal"
|
||||
phx-click={
|
||||
Phoenix.LiveView.JS.hide(to: "#search-modal")
|
||||
|> Phoenix.LiveView.JS.push("clear_search")
|
||||
@@ -360,10 +367,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
|
||||
<div
|
||||
class="search-modal-content w-full max-w-xl mx-4"
|
||||
style="background: var(--t-surface-raised); border-radius: var(--t-radius-card); overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);"
|
||||
phx-click-away={
|
||||
Phoenix.LiveView.JS.hide(to: "#search-modal")
|
||||
|> Phoenix.LiveView.JS.push("clear_search")
|
||||
}
|
||||
onclick="event.stopPropagation()"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-3 p-4"
|
||||
@@ -391,10 +395,20 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
|
||||
placeholder="Search products..."
|
||||
value={@search_query}
|
||||
phx-keyup="search"
|
||||
phx-debounce="300"
|
||||
phx-debounce="150"
|
||||
autocomplete="off"
|
||||
phx-click={Phoenix.LiveView.JS.dispatch("stop-propagation")}
|
||||
role="combobox"
|
||||
aria-expanded={to_string(@search_results != [])}
|
||||
aria-controls="search-results-list"
|
||||
aria-autocomplete="list"
|
||||
/>
|
||||
<div
|
||||
class="hidden sm:flex items-center gap-1 text-xs px-1.5 py-0.5 rounded"
|
||||
style="color: var(--t-text-tertiary); border: 1px solid var(--t-border-default);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<kbd>⌘</kbd><kbd>K</kbd>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="w-8 h-8 flex items-center justify-center transition-all"
|
||||
@@ -423,18 +437,20 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
|
||||
<div class="search-results" style="max-height: 60vh; overflow-y: auto;">
|
||||
<%= cond do %>
|
||||
<% @search_results != [] -> %>
|
||||
<ul class="py-2" role="listbox" aria-label="Search results">
|
||||
<li :for={item <- @results_with_images} role="option">
|
||||
<a
|
||||
href={"/products/#{item.product.slug || item.product.id}"}
|
||||
<ul id="search-results-list" class="py-2" role="listbox" aria-label="Search results">
|
||||
<li
|
||||
:for={item <- @results_with_images}
|
||||
id={"search-result-#{item.idx}"}
|
||||
role="option"
|
||||
aria-selected="false"
|
||||
>
|
||||
<.link
|
||||
navigate={"/products/#{item.product.slug || item.product.id}"}
|
||||
class="flex items-center gap-3 px-4 py-3 transition-colors"
|
||||
style="text-decoration: none; color: inherit;"
|
||||
onmouseenter="this.style.background='var(--t-surface-sunken)'"
|
||||
onmouseleave="this.style.background='transparent'"
|
||||
phx-click={
|
||||
Phoenix.LiveView.JS.hide(to: "#search-modal")
|
||||
|> Phoenix.LiveView.JS.push("clear_search")
|
||||
}
|
||||
phx-click={Phoenix.LiveView.JS.hide(to: "#search-modal")}
|
||||
>
|
||||
<div
|
||||
:if={item.image_url}
|
||||
@@ -459,7 +475,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
<% String.length(@search_query) >= 2 -> %>
|
||||
@@ -494,12 +510,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
|
||||
"""
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :mode, :atom, default: :live
|
||||
attr :categories, :list, default: []
|
||||
|
||||
def shop_footer(assigns) do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:current_year, Date.utc_today().year)
|
||||
|> assign(:categories, SimpleshopTheme.Theme.PreviewData.categories())
|
||||
assigns = assign(assigns, :current_year, Date.utc_today().year)
|
||||
|
||||
~H"""
|
||||
<footer style="background-color: var(--t-surface-raised); border-top: 1px solid var(--t-border-default);">
|
||||
|
||||
@@ -696,7 +696,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
>
|
||||
<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}');"}
|
||||
style={
|
||||
if(category[:image_url],
|
||||
do: "background-image: url('#{category.image_url}');",
|
||||
else: ""
|
||||
)
|
||||
}
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
@@ -714,7 +719,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
>
|
||||
<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}');"}
|
||||
style={
|
||||
if(category[:image_url],
|
||||
do: "background-image: url('#{category.image_url}');",
|
||||
else: ""
|
||||
)
|
||||
}
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user