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""" """ 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""" """ 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 %>
""" 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 %>
""" 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 %>
<.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 %> """ end attr :product, :map, required: true attr :theme_settings, :map, required: true attr :variant, :atom, required: true attr :show_category, :boolean, required: true attr :show_badges, :boolean, required: true attr :show_delivery_text, :boolean, required: true defp product_card_inner(assigns) do ~H"""
<%= if @show_badges do %> <.product_badge product={@product} /> <% end %> {@product.name} <%= if @theme_settings.hover_image && @product[:hover_image_url] do %> {@product.name} <% 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 %>

<% :compact -> %>

£<%= @product.price / 100 %>

<% :minimal -> %>

£<%= @product.price / 100 %>

<% end %> """ end defp variant_defaults(:default), do: %{show_category: true, show_badges: true, show_delivery_text: true, clickable: true} defp variant_defaults(:featured), do: %{show_category: false, show_badges: true, show_delivery_text: true, clickable: true} defp variant_defaults(:compact), do: %{show_category: false, show_badges: false, show_delivery_text: false, clickable: true} defp variant_defaults(:minimal), do: %{show_category: false, show_badges: false, show_delivery_text: false, clickable: false} defp card_classes(:default), do: "product-card group overflow-hidden transition-all" defp card_classes(:featured), do: "product-card group overflow-hidden transition-all hover:-translate-y-1" defp card_classes(:compact), do: "product-card group overflow-hidden cursor-pointer" defp card_classes(:minimal), do: "product-card group overflow-hidden" defp card_style(:default), do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card); cursor: pointer;" defp card_style(:featured), do: "background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;" defp card_style(:compact), do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);" defp card_style(:minimal), do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);" defp image_container_classes(:compact), do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative" defp image_container_classes(:minimal), do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative" defp image_container_classes(_), do: "product-image-container bg-gray-200 overflow-hidden relative" defp content_padding_class(:compact), do: "p-3" defp content_padding_class(:minimal), do: "p-2" defp content_padding_class(_), do: "" defp title_classes(:default), do: "font-semibold mb-2" defp title_classes(:featured), do: "text-sm font-medium mb-1" defp title_classes(:compact), do: "font-semibold text-sm mb-1" defp title_classes(:minimal), do: "text-xs font-semibold truncate" defp title_style(:default), do: "font-family: var(--t-font-heading); color: var(--t-text-primary);" defp title_style(:featured), do: "color: var(--t-text-primary);" defp title_style(:compact), do: "font-family: var(--t-font-heading); color: var(--t-text-primary);" defp title_style(:minimal), do: "color: var(--t-text-primary);" @doc """ Renders a responsive product grid container. This component wraps product cards in a responsive grid layout. It supports theme-based column settings or fixed column layouts for specific use cases. ## Attributes * `theme_settings` - Optional. When provided, uses `grid_columns` setting for lg breakpoint. * `columns` - Optional. Fixed column count for lg breakpoint (overrides theme_settings). Use `:fixed_4` for a fixed 2/4 column layout (error page, related products). * `gap` - Optional. Gap size. Defaults to standard gap. * `class` - Optional. Additional CSS classes to add. ## Slots * `inner_block` - Required. The product cards to render inside the grid. ## Examples <.product_grid theme_settings={@theme_settings}> <%= for product <- @products do %> <.product_card product={product} theme_settings={@theme_settings} /> <% end %> <.product_grid columns={:fixed_4} gap="gap-6"> ... """ attr :theme_settings, :map, default: nil attr :columns, :atom, default: nil attr :gap, :string, default: nil attr :class, :string, default: nil slot :inner_block, required: true def product_grid(assigns) do ~H"""
<%= render_slot(@inner_block) %>
""" end defp grid_classes(theme_settings, columns, gap, extra_class) do base = "product-grid grid" cols = cond do columns == :fixed_4 -> "grid-cols-2 md:grid-cols-4" theme_settings != nil -> responsive_cols = "grid-cols-1 sm:grid-cols-2" lg_cols = case theme_settings.grid_columns do "2" -> "lg:grid-cols-2" "3" -> "lg:grid-cols-3" "4" -> "lg:grid-cols-4" _ -> "lg:grid-cols-3" end "#{responsive_cols} #{lg_cols}" true -> "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" end gap_class = gap || "" [base, cols, gap_class, extra_class] |> Enum.reject(&is_nil/1) |> Enum.reject(&(&1 == "")) |> Enum.join(" ") end @doc """ Renders a centered hero section with title, description, and optional CTAs. ## Attributes * `title` - Required. The main heading text. * `description` - Required. The description paragraph text. * `variant` - The visual variant: - `:default` - Standard hero with section wrapper and padding - `:page` - Page header style, larger title, more spacing, no section wrapper - `:error` - Error page with pre-title (404), two buttons * `background` - Background style. Either `:base` (default) or `:sunken`. * `pre_title` - Optional. Text shown above title (e.g., "404" for error pages). * `cta_text` - Optional. Text for the primary CTA button. * `cta_page` - Optional. Page to navigate to on click (for preview mode). * `cta_href` - Optional. URL for live mode navigation. * `secondary_cta_text` - Optional. Text for secondary button (error variant). * `secondary_cta_page` - Optional. Page for secondary button (preview mode). * `secondary_cta_href` - Optional. URL for secondary button (live mode). * `mode` - Either `:live` (default) or `:preview`. ## Examples <.hero_section title="Original designs, printed on demand" description="From art prints to apparel..." cta_text="Shop the collection" cta_page="collection" mode={:preview} /> <.hero_section variant={:page} title="Contact Us" description="Questions about your order?" /> <.hero_section variant={:error} pre_title="404" title="Page Not Found" description="Sorry, we couldn't find the page..." cta_text="Go to Homepage" secondary_cta_text="Browse Products" mode={:preview} /> """ attr :title, :string, required: true attr :description, :string, required: true attr :variant, :atom, default: :default attr :background, :atom, default: :base attr :pre_title, :string, default: nil attr :cta_text, :string, default: nil attr :cta_page, :string, default: nil attr :cta_href, :string, default: nil attr :secondary_cta_text, :string, default: nil attr :secondary_cta_page, :string, default: nil attr :secondary_cta_href, :string, default: nil attr :mode, :atom, default: :live def hero_section(assigns) do ~H""" <%= case @variant do %> <% :default -> %>

<%= @title %>

<%= @description %>

<.hero_cta :if={@cta_text} text={@cta_text} page={@cta_page} href={@cta_href} mode={@mode} variant={:primary} />
<% :page -> %>

<%= @title %>

<%= @description %>

<% :error -> %>
<%= if @pre_title do %>

<%= @pre_title %>

<% end %>

<%= @title %>

<%= @description %>

<%= if @cta_text || @secondary_cta_text do %>
<.hero_cta :if={@cta_text} text={@cta_text} page={@cta_page} href={@cta_href} mode={@mode} variant={:primary} /> <.hero_cta :if={@secondary_cta_text} text={@secondary_cta_text} page={@secondary_cta_page} href={@secondary_cta_href} mode={@mode} variant={:secondary} />
<% end %>
<% end %> """ end attr :text, :string, required: true attr :page, :string, default: nil attr :href, :string, default: nil attr :mode, :atom, required: true attr :variant, :atom, required: true defp hero_cta(assigns) do ~H""" <%= if @mode == :preview do %> <% else %> " text-decoration: none;"} > <%= @text %> <% end %> """ end defp hero_cta_classes(:primary), do: "px-8 py-3 font-semibold transition-all" defp hero_cta_classes(:secondary), do: "px-8 py-3 font-semibold transition-all" defp hero_cta_style(:primary) do "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); border: none; cursor: pointer;" end defp hero_cta_style(:secondary) do "border: 2px solid var(--t-border-default); color: var(--t-text-primary); border-radius: var(--t-radius-button); background: transparent; cursor: pointer;" end end