fix navigation links, footer categories, product card structure, and social icons

- add missing cta_href to hero section and error page CTAs
- replace hardcoded footer shop links with real product categories
- restructure product cards with stretched-link pattern so category
  badges link to their collection page
- unify social icons: footer and contact page share the same default
  links from a single source in content.ex
- add search implementation plan (docs/plans/search.md, deferred)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-02-11 08:17:19 +00:00
parent ac46c1504f
commit 209ae7aee7
15 changed files with 186 additions and 161 deletions

View File

@ -53,11 +53,10 @@ Issues found during hands-on testing of the deployed prod site on mobile and des
- [ ] Shipping costs: add Stripe shipping options or query Printify for dynamic rates
### Navigation & links
- [ ] Search doesn't work (modal opens but no results/functionality)
- [ ] "Shop the collection" button/link does nothing
- [ ] Footer "New arrivals" and "Best sellers" links don't go anywhere
- [ ] Should be able to tap a category badge on product cards to go to that category
- [ ] Footer social icons should match the "Find me on" icons from the contact page
- [x] "Shop the collection" button/link does nothing
- [x] Footer "New arrivals" and "Best sellers" links don't go anywhere
- [x] Should be able to tap a category badge on product cards to go to that category
- [x] Footer social icons should match the "Find me on" icons from the contact page
### Collections / all products
- [ ] Categories on all-products page are too spaced out
@ -71,6 +70,9 @@ Issues found during hands-on testing of the deployed prod site on mobile and des
### Errors
- [ ] 404 page is broken
### Search (deferred — after usability fixes)
- [ ] Search doesn't work (modal opens but no results/functionality) — see [docs/plans/search.md](docs/plans/search.md)
## Roadmap
### Tier 1 — MVP (can take real orders and fulfil them)

View File

@ -229,6 +229,14 @@
outline-offset: 2px;
}
/* Stretched link: makes a parent container fully clickable via ::after overlay */
.stretched-link::after {
content: "";
position: absolute;
inset: 0;
z-index: 0;
}
/* Nav link styling with active state indicator */
.shop-nav a,
.shop-nav span {

61
docs/plans/search.md Normal file
View File

@ -0,0 +1,61 @@
# Plan: Implement product search in search modal
Status: Pending (after usability fixes)
## Overview
The search modal UI shell exists but has zero functionality — no event bindings, no backend search, no results rendering. This plan adds live search across all products.
## Approach
Small catalog (print-on-demand, < 100 products) search `PreviewData.products()` in memory. No DB full-text search needed.
Product maps have: `.name`, `.category`, `.description`, `.slug`, `.price`, `.image_url`, `.image_id`
## Changes
### 1. CartHook — add search assigns + event handler
**File:** `lib/simpleshop_theme_web/cart_hook.ex`
- Init assigns in `on_mount`: `search_results: []`, `search_query: ""`
- Handle `"search"` event (from `phx-keyup`):
- Empty/blank query → assign `search_results: []`, `search_query: ""`
- Non-empty → filter `PreviewData.products()` by name/category/description (case-insensitive substring match), take 6, assign results
- Handle `"close_search"` event → clear query + results + hide modal via JS
### 2. shop_layout + search_modal — add search attrs and UI
**File:** `lib/simpleshop_theme_web/components/shop_components/layout.ex`
**shop_layout:**
- Add optional attrs: `search_results` (default `[]`), `search_query` (default `""`)
- Pass them to `<.search_modal>`
**search_modal:**
- Add `search_results` (list, default `[]`) and `search_query` (string, default `""`) attrs
- Add `name="query"`, `phx-keyup="search"`, `phx-debounce="200"`, `value={@search_query}` to the input
- On close button + backdrop click: also push `"close_search"` event
- Results section below input:
- Each result: link to `/products/{slug}` with product name, category, formatted price
- "No results found" when query non-empty but no matches
- Hint text only shown when no query
- Click on result: navigate to product, close modal (JS.hide + close_search event)
### 3. Page templates — thread search assigns
**All 8 files in** `lib/simpleshop_theme_web/components/page_templates/`
Add two lines to each `<.shop_layout>` call:
```
search_results={assigns[:search_results] || []}
search_query={assigns[:search_query] || ""}
```
Same pattern as `cart_drawer_open` and `cart_status`.
## Files to modify
1. `lib/simpleshop_theme_web/cart_hook.ex`
2. `lib/simpleshop_theme_web/components/shop_components/layout.ex` (shop_layout + search_modal)
3. All 8 page templates in `lib/simpleshop_theme_web/components/page_templates/`
## Verification
- Browser: open search modal on multiple pages, type queries, verify results appear and link correctly
- `mix test` — all existing tests pass

View File

@ -19,6 +19,7 @@ defmodule SimpleshopThemeWeb.CartHook do
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3]
alias SimpleshopTheme.Cart
alias SimpleshopTheme.Theme.PreviewData
def on_mount(:mount_cart, _params, session, socket) do
cart_items = Cart.get_from_session(session)
@ -28,6 +29,7 @@ defmodule SimpleshopThemeWeb.CartHook do
|> update_cart_assigns(cart_items)
|> assign(:cart_drawer_open, false)
|> assign(:cart_status, nil)
|> assign(:categories, PreviewData.categories())
|> attach_hook(:cart_events, :handle_event, &handle_cart_event/3)
|> attach_hook(:cart_info, :handle_info, &handle_cart_info/2)

View File

@ -8,6 +8,7 @@
cart_subtotal={@cart_subtotal}
cart_drawer_open={assigns[:cart_drawer_open] || false}
cart_status={assigns[:cart_status]}
categories={assigns[:categories] || []}
active_page="cart"
>
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">

View File

@ -8,6 +8,7 @@
cart_subtotal={@cart_subtotal}
cart_drawer_open={assigns[:cart_drawer_open] || false}
cart_status={assigns[:cart_status]}
categories={assigns[:categories] || []}
active_page="checkout"
>
<main id="main-content" class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-16">

View File

@ -8,6 +8,7 @@
cart_subtotal={@cart_subtotal}
cart_drawer_open={assigns[:cart_drawer_open] || false}
cart_status={assigns[:cart_status]}
categories={assigns[:categories] || []}
active_page="collection"
>
<main id="main-content">

View File

@ -8,6 +8,7 @@
cart_subtotal={@cart_subtotal}
cart_drawer_open={assigns[:cart_drawer_open] || false}
cart_status={assigns[:cart_status]}
categories={assigns[:categories] || []}
active_page="contact"
>
<main id="main-content" class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
@ -34,13 +35,7 @@
<.newsletter_card />
<.social_links_card links={[
%{platform: :instagram, url: "https://instagram.com", label: "Instagram"},
%{platform: :bluesky, url: "https://bsky.app", label: "Bluesky"},
%{platform: :mastodon, url: "https://mastodon.social", label: "Mastodon"},
%{platform: :kofi, url: "https://ko-fi.com", label: "Ko-fi"},
%{platform: :github, url: "https://github.com", label: "GitHub"}
]} />
<.social_links_card />
</div>
</div>
</main>

View File

@ -8,6 +8,7 @@
cart_subtotal={@cart_subtotal}
cart_drawer_open={assigns[:cart_drawer_open] || false}
cart_status={assigns[:cart_status]}
categories={assigns[:categories] || []}
active_page={@active_page}
>
<main id="main-content" class="content-page" style="background-color: var(--t-surface-base);">

View File

@ -8,6 +8,7 @@
cart_subtotal={@cart_subtotal}
cart_drawer_open={assigns[:cart_drawer_open] || false}
cart_status={assigns[:cart_status]}
categories={assigns[:categories] || []}
active_page="error"
error_page
>
@ -24,8 +25,10 @@
description={@error_description}
cta_text="Go to Homepage"
cta_page="home"
cta_href="/"
secondary_cta_text="Browse Products"
secondary_cta_page="collection"
secondary_cta_href="/collections/all"
mode={@mode}
/>

View File

@ -8,6 +8,7 @@
cart_subtotal={@cart_subtotal}
cart_drawer_open={assigns[:cart_drawer_open] || false}
cart_status={assigns[:cart_status]}
categories={assigns[:categories] || []}
active_page="home"
>
<main id="main-content">
@ -16,6 +17,7 @@
description="Welcome to the SimpleShop demo store. This is where your hero text goes something short and punchy about what makes your shop worth a browse."
cta_text="Shop the collection"
cta_page="collection"
cta_href="/collections/all"
mode={@mode}
/>
@ -34,6 +36,7 @@
image_url="/mockups/mountain-sunrise-print-3-800.webp"
link_text="Learn more about the studio →"
link_page="about"
link_href="/about"
mode={@mode}
/>
</main>

View File

@ -8,6 +8,7 @@
cart_subtotal={@cart_subtotal}
cart_drawer_open={assigns[:cart_drawer_open] || false}
cart_status={assigns[:cart_status]}
categories={assigns[:categories] || []}
active_page="pdp"
>
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">

View File

@ -5,6 +5,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
import SimpleshopThemeWeb.ShopComponents.Base
@default_social_links [
%{platform: :instagram, url: "https://instagram.com", label: "Instagram"},
%{platform: :bluesky, url: "https://bsky.app", label: "Bluesky"},
%{platform: :mastodon, url: "https://mastodon.social", label: "Mastodon"},
%{platform: :kofi, url: "https://ko-fi.com", label: "Ko-fi"},
%{platform: :github, url: "https://github.com", label: "GitHub"}
]
@doc """
Renders a content body container for long-form content pages (about, etc.).
@ -338,11 +346,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
"""
attr :title, :string, default: "Find me on"
attr :links, :list,
default: [
%{platform: :instagram, url: "#", label: "Instagram"},
%{platform: :pinterest, url: "#", label: "Pinterest"}
]
attr :links, :list, default: @default_social_links
def social_links_card(assigns) do
~H"""
@ -381,11 +385,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Content do
<.social_links />
<.social_links links={[%{platform: :instagram, url: "https://instagram.com/example", label: "Instagram"}]} />
"""
attr :links, :list,
default: [
%{platform: :instagram, url: "https://instagram.com", label: "Instagram"},
%{platform: :pinterest, url: "https://pinterest.com", label: "Pinterest"}
]
attr :links, :list, default: @default_social_links
def social_links(assigns) do
~H"""

View File

@ -65,6 +65,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
attr :cart_subtotal, :string, required: true
attr :cart_drawer_open, :boolean, default: false
attr :cart_status, :string, default: nil
attr :categories, :list, default: []
attr :active_page, :string, required: true
attr :error_page, :boolean, default: false
@ -95,7 +96,7 @@ 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={@categories} />
<.cart_drawer
cart_items={@cart_items}
@ -405,6 +406,7 @@ 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 = assign(assigns, :current_year, Date.utc_today().year)
@ -437,6 +439,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
All products
</a>
</li>
<%= for category <- @categories do %>
<li>
<a
href="#"
@ -445,20 +448,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
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
{category.name}
</a>
</li>
<% end %>
<% else %>
<li>
<a
@ -469,25 +462,18 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
All products
</a>
</li>
<%= for category <- @categories do %>
<li>
<a
href="/collections/all"
href={"/collections/#{category.slug}"}
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
{category.name}
</a>
</li>
<% end %>
<% end %>
</ul>
</div>
<div>
@ -594,48 +580,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<p class="text-xs" style="color: var(--t-text-tertiary);">
© {@current_year} {@theme_settings.site_name}
</p>
<div class="flex gap-2">
<a
href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
class="social-link w-9 h-9 flex items-center justify-center transition-all"
style="color: var(--t-text-secondary); border-radius: var(--t-radius-button);"
aria-label="Instagram"
>
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
</a>
<a
href="https://pinterest.com"
target="_blank"
rel="noopener noreferrer"
class="social-link w-9 h-9 flex items-center justify-center transition-all"
style="color: var(--t-text-secondary); border-radius: var(--t-radius-button);"
aria-label="Pinterest"
>
<svg
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<path d="M8 12c0-2.2 1.8-4 4-4s4 1.8 4 4-1.8 4-4 4"></path>
<line x1="12" y1="16" x2="9" y2="21"></line>
</svg>
</a>
</div>
<.social_links />
</div>
</div>
</footer>

View File

@ -59,14 +59,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
end)
~H"""
<%= if @clickable_resolved do %>
<%= if @mode == :preview do %>
<a
href="#"
phx-click="change_preview_page"
phx-value-page="pdp"
<article
class={card_classes(@variant)}
style={card_style(@variant)}
style={card_style(@variant) <> if(@clickable_resolved, do: " position: relative;", else: "")}
>
<.product_card_inner
product={@product}
@ -76,41 +71,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
show_category={@show_category_resolved}
show_badges={@show_badges_resolved}
show_delivery_text={@show_delivery_text_resolved}
clickable={@clickable_resolved}
mode={@mode}
/>
</a>
<% else %>
<a
href={"/products/#{@product[:slug] || @product[:id]}"}
class={card_classes(@variant)}
style={card_style(@variant)}
>
<.product_card_inner
product={@product}
theme_settings={@theme_settings}
variant={@variant}
priority={@priority}
show_category={@show_category_resolved}
show_badges={@show_badges_resolved}
show_delivery_text={@show_delivery_text_resolved}
/>
</a>
<% end %>
<% else %>
<div
class={card_classes(@variant)}
style={card_style(@variant)}
>
<.product_card_inner
product={@product}
theme_settings={@theme_settings}
variant={@variant}
priority={@priority}
show_category={@show_category_resolved}
show_badges={@show_badges_resolved}
show_delivery_text={@show_delivery_text_resolved}
/>
</div>
<% end %>
</article>
"""
end
@ -121,6 +85,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
attr :show_category, :boolean, required: true
attr :show_badges, :boolean, required: true
attr :show_delivery_text, :boolean, required: true
attr :clickable, :boolean, default: true
attr :mode, :atom, default: :live
defp product_card_inner(assigns) do
assigns =
@ -171,12 +137,47 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
</div>
<div class={content_padding_class(@variant)}>
<%= if @show_category && @product[:category] do %>
<p class="text-xs mb-1" style="color: var(--t-text-tertiary);">
<%= if @mode == :preview do %>
<p
class="text-xs mb-1"
style="color: var(--t-text-tertiary); position: relative; z-index: 1;"
>
{@product.category}
</p>
<% else %>
<a
href={"/collections/#{Slug.slugify(@product.category)}"}
class="text-xs mb-1 block hover:underline"
style="color: var(--t-text-tertiary); text-decoration: none; position: relative; z-index: 1;"
>
{@product.category}
</a>
<% end %>
<% end %>
<h3 class={title_classes(@variant)} style={title_style(@variant)}>
<%= if @clickable do %>
<%= if @mode == :preview do %>
<a
href="#"
phx-click="change_preview_page"
phx-value-page="pdp"
class="stretched-link"
style="color: inherit; text-decoration: none;"
>
{@product.name}
</a>
<% else %>
<a
href={"/products/#{@product[:slug] || @product[:id]}"}
class="stretched-link"
style="color: inherit; text-decoration: none;"
>
{@product.name}
</a>
<% end %>
<% else %>
{@product.name}
<% end %>
</h3>
<%= if @theme_settings.show_prices do %>
<.product_price product={@product} variant={@variant} />