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:
jamey
2026-02-13 08:27:26 +00:00
parent 037cd168cd
commit 57c3ba0e28
22 changed files with 745 additions and 330 deletions

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}
/>

View File

@@ -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);">

View File

@@ -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