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"""
{@message}
"""
end
@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"""
Skip to main content
"""
end
@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"""
<%= if @hint_text do %>
{@hint_text}
<% end %>
"""
end
@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"""
"""
end
@doc """
Renders the shop header with logo, navigation, and actions.
## Attributes
* `theme_settings` - Required. The theme settings map.
* `logo_image` - Optional. The logo image struct (with id, is_svg fields).
* `header_image` - Optional. The header background image struct.
* `active_page` - Optional. Current page for nav highlighting.
* `mode` - Optional. Either `:live` (default) or `:preview`.
* `cart_count` - Optional. Number of items in cart. Defaults to 0.
## Examples
<.shop_header theme_settings={@theme_settings} />
<.shop_header theme_settings={@theme_settings} mode={:preview} cart_count={2} />
"""
attr :theme_settings, :map, required: true
attr :logo_image, :map, default: nil
attr :header_image, :map, default: nil
attr :active_page, :string, default: nil
attr :mode, :atom, default: :live
attr :cart_count, :integer, default: 0
def shop_header(assigns) do
~H"""
<%= if @theme_settings.header_background_enabled && @header_image do %>
<% end %>
<%= case @theme_settings.logo_mode do %>
<% "text-only" -> %>
<%= @theme_settings.site_name %>
<% "logo-text" -> %>
<%= if @logo_image do %>
<% end %>
<%= @theme_settings.site_name %>
<% "logo-only" -> %>
<%= if @logo_image do %>
<% else %>
<%= @theme_settings.site_name %>
<% end %>
<% _ -> %>
<%= @theme_settings.site_name %>
<% end %>
"""
end
defp logo_url(logo_image, %{logo_recolor: true, logo_color: color}) when logo_image.is_svg do
clean_color = String.trim_leading(color, "#")
"/images/#{logo_image.id}/recolored/#{clean_color}"
end
defp logo_url(logo_image, _), do: "/images/#{logo_image.id}"
defp header_background_style(settings, header_image) do
"position: absolute; top: 0; left: 0; right: 0; bottom: 0; " <>
"background-image: url('/images/#{header_image.id}'); " <>
"background-size: #{settings.header_zoom}%; " <>
"background-position: #{settings.header_position_x}% #{settings.header_position_y}%; " <>
"background-repeat: no-repeat; z-index: 0;"
end
@doc """
Renders the cart drawer (floating sidebar).
The drawer slides in from the right when opened. It displays cart items
and checkout options.
## Attributes
* `cart_items` - List of cart items to display. Each item should have
`image`, `name`, `variant`, and `price` keys. Default: []
* `subtotal` - The subtotal to display. Default: nil (shows "£0.00")
* `mode` - Either `:live` (default) for real stores or `:preview` for theme editor.
In preview mode, "View basket" navigates via LiveView JS commands.
## Examples
<.cart_drawer cart_items={@cart.items} subtotal={@cart.subtotal} />
<.cart_drawer cart_items={demo_items} subtotal="£72.00" mode={:preview} />
"""
attr :cart_items, :list, default: []
attr :subtotal, :string, default: nil
attr :mode, :atom, default: :live
def cart_drawer(assigns) do
assigns = assign_new(assigns, :display_subtotal, fn ->
assigns.subtotal || "£0.00"
end)
~H"""
Your basket
<%= for item <- @cart_items do %>
<%= item.name %>
<%= item.variant %>
<%= item.price %>
<% end %>
Phoenix.LiveView.JS.remove_class("open", to: "#cart-drawer-overlay")}
>
"""
end
@doc """
Renders a product card with configurable variants.
## Attributes
* `product` - Required. The product map with `name`, `image_url`, `price`, etc.
* `theme_settings` - Required. The theme settings map.
* `mode` - Either `:live` (default) or `:preview`.
* `variant` - The visual variant:
- `:default` - Collection page style with border, category, full details
- `:featured` - Home page style with hover lift, no border
- `:compact` - PDP related products with aspect-square, minimal info
- `:minimal` - Error 404 style, smallest, not clickable
* `show_category` - Show category label. Defaults based on variant.
* `show_badges` - Show product badges. Defaults based on variant.
* `show_delivery_text` - Show "Free delivery" text. Defaults based on variant.
* `clickable` - Whether the card navigates. Defaults based on variant.
## Examples
<.product_card product={product} theme_settings={@theme_settings} />
<.product_card product={product} theme_settings={@theme_settings} variant={:featured} mode={:preview} />
"""
attr :product, :map, required: true
attr :theme_settings, :map, required: true
attr :mode, :atom, default: :live
attr :variant, :atom, default: :default
attr :show_category, :boolean, default: nil
attr :show_badges, :boolean, default: nil
attr :show_delivery_text, :boolean, default: nil
attr :clickable, :boolean, default: nil
def product_card(assigns) do
# Apply variant defaults for nil values
defaults = variant_defaults(assigns.variant)
assigns =
assigns
|> assign_new(:show_category_resolved, fn ->
if assigns.show_category == nil, do: defaults.show_category, else: assigns.show_category
end)
|> assign_new(:show_badges_resolved, fn ->
if assigns.show_badges == nil, do: defaults.show_badges, else: assigns.show_badges
end)
|> assign_new(:show_delivery_text_resolved, fn ->
if assigns.show_delivery_text == nil,
do: defaults.show_delivery_text,
else: assigns.show_delivery_text
end)
|> assign_new(:clickable_resolved, fn ->
if assigns.clickable == nil, do: defaults.clickable, else: assigns.clickable
end)
~H"""
<%= if @clickable_resolved do %>
<%= if @mode == :preview do %>
<.product_card_inner
product={@product}
theme_settings={@theme_settings}
variant={@variant}
show_category={@show_category_resolved}
show_badges={@show_badges_resolved}
show_delivery_text={@show_delivery_text_resolved}
/>
<% else %>
<.product_card_inner
product={@product}
theme_settings={@theme_settings}
variant={@variant}
show_category={@show_category_resolved}
show_badges={@show_badges_resolved}
show_delivery_text={@show_delivery_text_resolved}
/>
<% end %>
<% else %>
<%= if @show_badges do %>
<.product_badge product={@product} />
<% end %>
<%= if @theme_settings.hover_image && @product[:hover_image_url] do %>
<% end %>
<%= if @show_category && @product[:category] do %>
<%= @product.category %>
<% end %>
<%= @product.name %>
<%= if @theme_settings.show_prices do %>
<.product_price product={@product} variant={@variant} />
<% end %>
<%= if @show_delivery_text do %>
Free delivery over £40
<% end %>
"""
end
attr :product, :map, required: true
defp product_badge(assigns) do
~H"""
<%= cond do %>
<% Map.get(@product, :in_stock, true) == false -> %>
Sold out
<% @product[:is_new] -> %>
New
<% @product[:on_sale] -> %>
Sale
<% true -> %>
<% end %>
"""
end
attr :product, :map, required: true
attr :variant, :atom, required: true
defp product_price(assigns) do
~H"""
<%= case @variant do %>
<% :default -> %>
<%= if @product.on_sale do %>
£<%= @product.price / 100 %>
£<%= @product.compare_at_price / 100 %>
<% else %>
£<%= @product.price / 100 %>
<% end %>
<% :featured -> %>
<%= if @product.on_sale do %>
£<%= @product.compare_at_price / 100 %>
<% end %>
£<%= @product.price / 100 %>
"""
end
@doc """
Renders a contact form card.
## Attributes
* `title` - Optional. Form heading. Defaults to "Send us a message".
## Examples
<.contact_form />
<.contact_form title="Get in touch" />
"""
attr :title, :string, default: "Send us a message"
def contact_form(assigns) do
~H"""
<%= @title %>
"""
end
@doc """
Renders the order tracking card.
## Examples
<.order_tracking_card />
"""
def order_tracking_card(assigns) do
~H"""
Track your order
Enter your email and we'll send you a link to check your order status.
"""
end
@doc """
Renders the info card with bullet points (e.g., "Handy to know" section).
## Attributes
* `title` - Required. Card heading.
* `items` - Required. List of maps with `label` and `value` keys.
## Examples
<.info_card title="Handy to know" items={[
%{label: "Printing", value: "2-5 business days"},
%{label: "Delivery", value: "3-7 business days after printing"}
]} />
"""
attr :title, :string, required: true
attr :items, :list, required: true
def info_card(assigns) do
~H"""
<%= @title %>
<%= for item <- @items do %>
•<%= item.label %>: <%= item.value %>
<% end %>
"""
end
@doc """
Renders the contact info card with email link.
## Attributes
* `title` - Optional. Card heading. Defaults to "Get in touch".
* `email` - Required. Email address.
* `response_text` - Optional. Response time text. Defaults to "We typically respond within 24 hours".
## Examples
<.contact_info_card email="hello@example.com" />
"""
attr :title, :string, default: "Get in touch"
attr :email, :string, required: true
attr :response_text, :string, default: "We typically respond within 24 hours"
def contact_info_card(assigns) do
~H"""