2026-01-17 13:11:39 +00:00
|
|
|
defmodule SimpleshopThemeWeb.ShopComponents do
|
|
|
|
|
@moduledoc """
|
|
|
|
|
Provides shop/storefront UI components.
|
|
|
|
|
|
|
|
|
|
These components are shared between the theme preview system and
|
|
|
|
|
the public storefront pages. They render using CSS custom properties
|
|
|
|
|
defined by the theme settings.
|
|
|
|
|
"""
|
|
|
|
|
use Phoenix.Component
|
|
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Renders the announcement bar.
|
|
|
|
|
|
|
|
|
|
The bar displays promotional messaging at the top of the page.
|
|
|
|
|
It uses CSS custom properties for theming.
|
|
|
|
|
|
|
|
|
|
## Attributes
|
|
|
|
|
|
|
|
|
|
* `theme_settings` - Required. The theme settings map.
|
|
|
|
|
* `message` - Optional. The announcement message to display.
|
|
|
|
|
Defaults to "Free delivery on orders over £40".
|
|
|
|
|
|
|
|
|
|
## Examples
|
|
|
|
|
|
|
|
|
|
<.announcement_bar theme_settings={@theme_settings} />
|
|
|
|
|
<.announcement_bar theme_settings={@theme_settings} message="20% off this weekend!" />
|
|
|
|
|
"""
|
|
|
|
|
attr :theme_settings, :map, required: true
|
|
|
|
|
attr :message, :string, default: "Free delivery on orders over £40"
|
|
|
|
|
|
|
|
|
|
def announcement_bar(assigns) do
|
|
|
|
|
~H"""
|
|
|
|
|
<div
|
|
|
|
|
class="announcement-bar"
|
|
|
|
|
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); text-align: center; padding: 0.5rem 1rem; font-size: var(--t-text-small);"
|
|
|
|
|
>
|
|
|
|
|
<p style="margin: 0;">{@message}</p>
|
|
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
end
|
2026-01-17 13:51:15 +00:00
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Renders the skip link for keyboard navigation accessibility.
|
|
|
|
|
|
|
|
|
|
This is a standard accessibility pattern that allows keyboard users
|
|
|
|
|
to skip directly to the main content.
|
|
|
|
|
"""
|
|
|
|
|
def skip_link(assigns) do
|
|
|
|
|
~H"""
|
|
|
|
|
<a href="#main-content" class="skip-link">
|
|
|
|
|
Skip to main content
|
|
|
|
|
</a>
|
|
|
|
|
"""
|
|
|
|
|
end
|
2026-01-17 14:05:00 +00:00
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Renders the search modal overlay.
|
|
|
|
|
|
|
|
|
|
This is a modal dialog for searching products. Currently provides
|
|
|
|
|
the UI shell; search functionality will be added later.
|
|
|
|
|
|
|
|
|
|
## Attributes
|
|
|
|
|
|
|
|
|
|
* `hint_text` - Optional. Hint text shown below the search input.
|
|
|
|
|
Defaults to nil (no hint shown).
|
|
|
|
|
|
|
|
|
|
## Examples
|
|
|
|
|
|
|
|
|
|
<.search_modal />
|
|
|
|
|
<.search_modal hint_text="Try searching for \"mountain\" or \"forest\"" />
|
|
|
|
|
"""
|
|
|
|
|
attr :hint_text, :string, default: nil
|
|
|
|
|
|
|
|
|
|
def search_modal(assigns) do
|
|
|
|
|
~H"""
|
|
|
|
|
<div
|
|
|
|
|
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-click={Phoenix.LiveView.JS.hide(to: "#search-modal")}
|
|
|
|
|
>
|
|
|
|
|
<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")}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
class="flex items-center gap-3 p-4"
|
|
|
|
|
style="border-bottom: 1px solid var(--t-border-default);"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5 flex-shrink-0" style="color: var(--t-text-tertiary);" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
|
|
|
<path d="M21 21l-4.35-4.35"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
id="search-input"
|
|
|
|
|
class="flex-1 text-lg bg-transparent border-none outline-none"
|
|
|
|
|
style="font-family: var(--t-font-body); color: var(--t-text-primary);"
|
|
|
|
|
placeholder="Search products..."
|
|
|
|
|
phx-click={Phoenix.LiveView.JS.dispatch("stop-propagation")}
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="w-8 h-8 flex items-center justify-center transition-all"
|
|
|
|
|
style="color: var(--t-text-tertiary); background: none; border: none; cursor: pointer; border-radius: var(--t-radius-button);"
|
|
|
|
|
phx-click={Phoenix.LiveView.JS.hide(to: "#search-modal")}
|
|
|
|
|
aria-label="Close search"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
|
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<%= if @hint_text do %>
|
|
|
|
|
<div class="p-6" style="color: var(--t-text-tertiary);">
|
|
|
|
|
<p class="text-sm">{@hint_text}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<% end %>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
end
|
2026-01-17 14:06:28 +00:00
|
|
|
|
|
|
|
|
@doc """
|
|
|
|
|
Renders the shop footer with newsletter signup and links.
|
|
|
|
|
|
|
|
|
|
## Attributes
|
|
|
|
|
|
|
|
|
|
* `theme_settings` - Required. The theme settings map containing site_name.
|
|
|
|
|
* `mode` - Optional. Either `:live` (default) for real navigation or
|
|
|
|
|
`:preview` for theme preview mode with phx-click handlers.
|
|
|
|
|
|
|
|
|
|
## Examples
|
|
|
|
|
|
|
|
|
|
<.shop_footer theme_settings={@theme_settings} />
|
|
|
|
|
<.shop_footer theme_settings={@theme_settings} mode={:preview} />
|
|
|
|
|
"""
|
|
|
|
|
attr :theme_settings, :map, required: true
|
|
|
|
|
attr :mode, :atom, default: :live
|
|
|
|
|
|
|
|
|
|
def shop_footer(assigns) do
|
|
|
|
|
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);">
|
|
|
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-12">
|
|
|
|
|
<!-- Newsletter -->
|
|
|
|
|
<div>
|
|
|
|
|
<h3 class="text-xl font-bold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
|
|
|
|
|
Stay in touch
|
|
|
|
|
</h3>
|
|
|
|
|
<p class="mb-4 text-sm" style="color: var(--t-text-secondary);">
|
|
|
|
|
Get 10% off your first order and be the first to know about new designs.
|
|
|
|
|
</p>
|
|
|
|
|
<form class="flex flex-wrap gap-2">
|
|
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
placeholder="your@email.com"
|
|
|
|
|
class="flex-1 min-w-0 px-4 py-2 text-sm"
|
|
|
|
|
style="background-color: var(--t-surface-base); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input); min-width: 150px;"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
class="px-6 py-2 font-medium transition-all text-sm whitespace-nowrap"
|
|
|
|
|
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); border-radius: var(--t-radius-button);"
|
|
|
|
|
>
|
|
|
|
|
Subscribe
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Links -->
|
|
|
|
|
<div class="grid grid-cols-2 gap-8">
|
|
|
|
|
<div>
|
|
|
|
|
<h4 class="font-semibold mb-4 text-sm" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
|
|
|
|
Shop
|
|
|
|
|
</h4>
|
|
|
|
|
<ul class="space-y-2 text-sm">
|
|
|
|
|
<%= if @mode == :preview 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;">All products</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>
|
|
|
|
|
<% 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>
|
|
|
|
|
<% end %>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h4 class="font-semibold mb-4 text-sm" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
|
|
|
|
Help
|
|
|
|
|
</h4>
|
|
|
|
|
<ul class="space-y-2 text-sm">
|
|
|
|
|
<%= if @mode == :preview do %>
|
|
|
|
|
<li><a href="#" phx-click="change_preview_page" phx-value-page="about" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary); cursor: pointer;">Delivery</a></li>
|
|
|
|
|
<li><a href="#" phx-click="change_preview_page" phx-value-page="about" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary); cursor: pointer;">Returns</a></li>
|
|
|
|
|
<li><a href="#" phx-click="change_preview_page" phx-value-page="contact" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary); cursor: pointer;">Contact</a></li>
|
|
|
|
|
<% else %>
|
|
|
|
|
<li><a href="/delivery" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">Delivery</a></li>
|
|
|
|
|
<li><a href="/returns" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">Returns</a></li>
|
|
|
|
|
<li><a href="/contact" class="transition-colors hover:opacity-80" style="color: var(--t-text-secondary);">Contact</a></li>
|
|
|
|
|
<% end %>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Bottom Bar -->
|
|
|
|
|
<div class="mt-12 pt-8 flex flex-col md:flex-row justify-between items-center gap-4" style="border-top: 1px solid var(--t-border-subtle);">
|
|
|
|
|
<p class="text-xs" style="color: var(--t-text-tertiary);">
|
|
|
|
|
© {@current_year} {@theme_settings.site_name}
|
|
|
|
|
</p>
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<a
|
|
|
|
|
href="#"
|
|
|
|
|
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="#"
|
|
|
|
|
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>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
|
|
|
|
"""
|
|
|
|
|
end
|
2026-01-17 13:11:39 +00:00
|
|
|
end
|