refactor: extract remaining page sections to ShopComponents
Add reusable components for all page sections: - category_nav: Category circles for homepage navigation - featured_products_section: Product grid with title and view-all CTA - image_text_section: Two-column image + text layout - collection_header: Page header with title and product count - filter_bar: Category pills and sort dropdown - content_body: Long-form content wrapper for about page - contact_form: Contact form card - order_tracking_card: Order tracking input card - info_card: Bullet-point info card - contact_info_card: Email contact card with icon - social_links: Social media icon links - cart_item: Cart item row with quantity controls - order_summary: Order totals and checkout card - breadcrumb: Navigation breadcrumb trail - related_products_section: Related products grid All preview pages now compose entirely of reusable components, making them easier to maintain and enabling future section-based theme customization. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1589ebaeca
commit
0c15929c19
@ -1043,4 +1043,846 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
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
|
||||
|
||||
@doc """
|
||||
Renders a row of category circles for navigation.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `categories` - Required. List of category maps with `name` and `image_url`.
|
||||
* `limit` - Optional. Maximum number of categories to show. Defaults to 3.
|
||||
* `mode` - Either `:live` (default) or `:preview`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.category_nav categories={@categories} mode={:preview} />
|
||||
<.category_nav categories={@categories} limit={4} />
|
||||
"""
|
||||
attr :categories, :list, required: true
|
||||
attr :limit, :integer, default: 3
|
||||
attr :mode, :atom, default: :live
|
||||
|
||||
def category_nav(assigns) do
|
||||
~H"""
|
||||
<section style="padding: var(--space-xl) var(--space-lg); background-color: var(--t-surface-base);">
|
||||
<nav class="grid grid-cols-3 gap-4 max-w-3xl mx-auto" aria-label="Product categories">
|
||||
<%= for category <- Enum.take(@categories, @limit) do %>
|
||||
<%= if @mode == :preview do %>
|
||||
<a href="#" phx-click="change_preview_page" phx-value-page="collection" class="flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5" style="text-decoration: none; cursor: pointer;">
|
||||
<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}');"}
|
||||
></div>
|
||||
<span class="text-sm font-medium" style="font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<%= category.name %>
|
||||
</span>
|
||||
</a>
|
||||
<% else %>
|
||||
<a href={"/products?category=#{category[:slug] || category.name}"} class="flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5" style="text-decoration: none;">
|
||||
<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}');"}
|
||||
></div>
|
||||
<span class="text-sm font-medium" style="font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<%= category.name %>
|
||||
</span>
|
||||
</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</nav>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a featured products section with title, product grid, and view-all button.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `title` - Required. Section heading text.
|
||||
* `products` - Required. List of products to display.
|
||||
* `theme_settings` - Required. The theme settings map.
|
||||
* `limit` - Optional. Maximum products to show. Defaults to 4.
|
||||
* `mode` - Either `:live` (default) or `:preview`.
|
||||
* `cta_text` - Optional. Text for the "view all" button. Defaults to "View all products".
|
||||
* `cta_page` - Optional. Page to navigate to (preview mode). Defaults to "collection".
|
||||
* `cta_href` - Optional. URL for live mode. Defaults to "/products".
|
||||
|
||||
## Examples
|
||||
|
||||
<.featured_products_section
|
||||
title="Featured products"
|
||||
products={@products}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
/>
|
||||
"""
|
||||
attr :title, :string, required: true
|
||||
attr :products, :list, required: true
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :limit, :integer, default: 4
|
||||
attr :mode, :atom, default: :live
|
||||
attr :cta_text, :string, default: "View all products"
|
||||
attr :cta_page, :string, default: "collection"
|
||||
attr :cta_href, :string, default: "/products"
|
||||
|
||||
def featured_products_section(assigns) do
|
||||
~H"""
|
||||
<section style="padding: var(--space-xl) var(--space-lg); background-color: var(--t-surface-sunken);">
|
||||
<h2 class="text-2xl mb-6" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking); color: var(--t-text-primary);">
|
||||
<%= @title %>
|
||||
</h2>
|
||||
|
||||
<.product_grid theme_settings={@theme_settings}>
|
||||
<%= for product <- Enum.take(@products, @limit) do %>
|
||||
<.product_card
|
||||
product={product}
|
||||
theme_settings={@theme_settings}
|
||||
mode={@mode}
|
||||
variant={:featured}
|
||||
/>
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
|
||||
<div class="text-center mt-8">
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@cta_page}
|
||||
class="px-6 py-3 font-medium transition-all"
|
||||
style="background-color: transparent; color: var(--t-text-primary); border: 1px solid var(--t-text-primary); border-radius: var(--t-radius-button); cursor: pointer;"
|
||||
>
|
||||
<%= @cta_text %>
|
||||
</button>
|
||||
<% else %>
|
||||
<a
|
||||
href={@cta_href}
|
||||
class="inline-block px-6 py-3 font-medium transition-all"
|
||||
style="background-color: transparent; color: var(--t-text-primary); border: 1px solid var(--t-text-primary); border-radius: var(--t-radius-button); text-decoration: none;"
|
||||
>
|
||||
<%= @cta_text %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a two-column image + text section.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `title` - Required. Section heading text.
|
||||
* `description` - Required. Body text content.
|
||||
* `image_url` - Required. URL for the image.
|
||||
* `link_text` - Optional. Text for the link. If nil, no link is shown.
|
||||
* `link_page` - Optional. Page to navigate to (preview mode).
|
||||
* `link_href` - Optional. URL for live mode.
|
||||
* `image_position` - Either `:left` (default) or `:right`.
|
||||
* `mode` - Either `:live` (default) or `:preview`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.image_text_section
|
||||
title="Made with passion, printed with care"
|
||||
description="Every design starts with an idea..."
|
||||
image_url="/mockups/example.jpg"
|
||||
link_text="Learn more about the studio →"
|
||||
link_page="about"
|
||||
mode={:preview}
|
||||
/>
|
||||
"""
|
||||
attr :title, :string, required: true
|
||||
attr :description, :string, required: true
|
||||
attr :image_url, :string, required: true
|
||||
attr :link_text, :string, default: nil
|
||||
attr :link_page, :string, default: nil
|
||||
attr :link_href, :string, default: nil
|
||||
attr :image_position, :atom, default: :left
|
||||
attr :mode, :atom, default: :live
|
||||
|
||||
def image_text_section(assigns) do
|
||||
~H"""
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center" style="padding: var(--space-2xl) var(--space-lg); background-color: var(--t-surface-base);">
|
||||
<%= if @image_position == :left do %>
|
||||
<.image_text_image image_url={@image_url} />
|
||||
<.image_text_content title={@title} description={@description} link_text={@link_text} link_page={@link_page} link_href={@link_href} mode={@mode} />
|
||||
<% else %>
|
||||
<.image_text_content title={@title} description={@description} link_text={@link_text} link_page={@link_page} link_href={@link_href} mode={@mode} />
|
||||
<.image_text_image image_url={@image_url} />
|
||||
<% end %>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :image_url, :string, required: true
|
||||
|
||||
defp image_text_image(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="h-72 rounded-lg bg-cover bg-center"
|
||||
style={"background-image: url('#{@image_url}'); border-radius: var(--t-radius-image);"}
|
||||
></div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :title, :string, required: true
|
||||
attr :description, :string, required: true
|
||||
attr :link_text, :string, default: nil
|
||||
attr :link_page, :string, default: nil
|
||||
attr :link_href, :string, default: nil
|
||||
attr :mode, :atom, required: true
|
||||
|
||||
defp image_text_content(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<h2 class="text-2xl mb-4" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking); color: var(--t-text-primary);">
|
||||
<%= @title %>
|
||||
</h2>
|
||||
<p class="text-base mb-4" style="color: var(--t-text-secondary); line-height: 1.7;">
|
||||
<%= @description %>
|
||||
</p>
|
||||
<%= if @link_text do %>
|
||||
<%= if @mode == :preview do %>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@link_page}
|
||||
class="text-sm font-medium transition-colors"
|
||||
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); text-decoration: none; cursor: pointer;"
|
||||
>
|
||||
<%= @link_text %>
|
||||
</a>
|
||||
<% else %>
|
||||
<a
|
||||
href={@link_href || "/"}
|
||||
class="text-sm font-medium transition-colors"
|
||||
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); text-decoration: none;"
|
||||
>
|
||||
<%= @link_text %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a page header with title and optional product count.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `title` - Required. The page title.
|
||||
* `subtitle` - Optional. Text below the title.
|
||||
* `product_count` - Optional. Number to display as "X products".
|
||||
|
||||
## Examples
|
||||
|
||||
<.collection_header title="All Products" product_count={24} />
|
||||
"""
|
||||
attr :title, :string, required: true
|
||||
attr :subtitle, :string, default: nil
|
||||
attr :product_count, :integer, default: nil
|
||||
|
||||
def collection_header(assigns) do
|
||||
~H"""
|
||||
<div class="border-b" style="background-color: var(--t-surface-raised); border-color: var(--t-border-default);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">
|
||||
<%= @title %>
|
||||
</h1>
|
||||
<%= if @subtitle do %>
|
||||
<p style="color: var(--t-text-secondary);"><%= @subtitle %></p>
|
||||
<% end %>
|
||||
<%= if @product_count do %>
|
||||
<p style="color: var(--t-text-secondary);"><%= @product_count %> products</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a filter bar with category pills and sort dropdown.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `categories` - Required. List of category maps with `name` field.
|
||||
* `active_category` - Optional. Currently selected category name. Defaults to "All".
|
||||
* `sort_options` - Optional. List of sort option strings.
|
||||
|
||||
## Examples
|
||||
|
||||
<.filter_bar categories={@categories} />
|
||||
<.filter_bar categories={@categories} active_category="Art Prints" />
|
||||
"""
|
||||
attr :categories, :list, required: true
|
||||
attr :active_category, :string, default: "All"
|
||||
attr :sort_options, :list, default: ["Sort by: Featured", "Price: Low to High", "Price: High to Low", "Newest", "Best Selling"]
|
||||
|
||||
def filter_bar(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<!-- Category Pills -->
|
||||
<div class="filter-pills-container flex gap-2 overflow-x-auto">
|
||||
<button class={"filter-pill#{if @active_category == "All", do: " filter-pill-active", else: ""}"}>
|
||||
All
|
||||
</button>
|
||||
<%= for category <- @categories do %>
|
||||
<button class={"filter-pill#{if @active_category == category.name, do: " filter-pill-active", else: ""}"}>
|
||||
<%= category.name %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Sort Dropdown -->
|
||||
<select
|
||||
class="px-4 py-2"
|
||||
style="background-color: var(--t-surface-raised); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input);"
|
||||
>
|
||||
<%= for option <- @sort_options do %>
|
||||
<option><%= option %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a content body container for long-form content pages (about, etc.).
|
||||
|
||||
## Attributes
|
||||
|
||||
* `image_url` - Optional. Header image URL.
|
||||
|
||||
## Slots
|
||||
|
||||
* `inner_block` - Required. The content to render.
|
||||
|
||||
## Examples
|
||||
|
||||
<.content_body image_url="/images/about.jpg">
|
||||
<p>Content here...</p>
|
||||
</.content_body>
|
||||
"""
|
||||
attr :image_url, :string, default: nil
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def content_body(assigns) do
|
||||
~H"""
|
||||
<div class="content-body" style="padding: var(--space-xl) var(--space-lg); max-width: 800px; margin: 0 auto;">
|
||||
<%= if @image_url do %>
|
||||
<div
|
||||
class="content-image about-image"
|
||||
style={"width: 100%; height: 300px; border-radius: var(--t-radius-image); margin-bottom: var(--space-lg); background-size: cover; background-position: center; background-image: url('#{@image_url}');"}
|
||||
></div>
|
||||
<% end %>
|
||||
|
||||
<div class="content-text" style="line-height: 1.7;">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
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"""
|
||||
<div
|
||||
class="p-8"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h2 class="text-xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
||||
<%= @title %>
|
||||
</h2>
|
||||
|
||||
<form class="space-y-4">
|
||||
<div>
|
||||
<label class="block font-medium mb-2" style="color: var(--t-text-primary);">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Your name"
|
||||
class="w-full px-4 py-2"
|
||||
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);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-medium mb-2" style="color: var(--t-text-primary);">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
class="w-full px-4 py-2"
|
||||
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);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-medium mb-2" style="color: var(--t-text-primary);">
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="How can we help?"
|
||||
class="w-full px-4 py-2"
|
||||
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);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-medium mb-2" style="color: var(--t-text-primary);">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
rows="5"
|
||||
placeholder="Your message..."
|
||||
class="w-full px-4 py-2"
|
||||
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);"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full px-6 py-3 font-semibold transition-all"
|
||||
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);"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the order tracking card.
|
||||
|
||||
## Examples
|
||||
|
||||
<.order_tracking_card />
|
||||
"""
|
||||
def order_tracking_card(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Track your order</h3>
|
||||
<p class="text-sm mb-3" style="color: var(--t-text-secondary);">
|
||||
Enter your email and we'll send you a link to check your order status.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
class="flex-1 min-w-0 px-3 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
|
||||
class="px-4 py-2 text-sm font-medium 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);"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
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"""
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);"><%= @title %></h3>
|
||||
<ul class="space-y-2 text-sm" style="color: var(--t-text-secondary);">
|
||||
<%= for item <- @items do %>
|
||||
<li class="flex items-start gap-2">
|
||||
<span style="color: var(--t-text-tertiary);">•</span>
|
||||
<span><strong style="color: var(--t-text-primary);"><%= item.label %>:</strong> <%= item.value %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
"""
|
||||
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"""
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);"><%= @title %></h3>
|
||||
<a href={"mailto:#{@email}"} class="flex items-center gap-2 mb-2" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); text-decoration: none;">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<%= @email %>
|
||||
</a>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||
<%= @response_text %>
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders social media icon links.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `links` - Optional. List of maps with `platform`, `url`, and `label` keys.
|
||||
Supported platforms: :instagram, :pinterest
|
||||
|
||||
## Examples
|
||||
|
||||
<.social_links />
|
||||
<.social_links links={[%{platform: :instagram, url: "https://instagram.com/example", label: "Instagram"}]} />
|
||||
"""
|
||||
attr :links, :list, default: [
|
||||
%{platform: :instagram, url: "#", label: "Instagram"},
|
||||
%{platform: :pinterest, url: "#", label: "Pinterest"}
|
||||
]
|
||||
|
||||
def social_links(assigns) do
|
||||
~H"""
|
||||
<div class="flex gap-4 justify-center">
|
||||
<%= for link <- @links do %>
|
||||
<a
|
||||
href={link.url}
|
||||
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={link.label}
|
||||
>
|
||||
<.social_icon platform={link.platform} />
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :platform, :atom, required: true
|
||||
|
||||
defp social_icon(%{platform: :instagram} = assigns) do
|
||||
~H"""
|
||||
<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>
|
||||
"""
|
||||
end
|
||||
|
||||
defp social_icon(%{platform: :pinterest} = assigns) do
|
||||
~H"""
|
||||
<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>
|
||||
"""
|
||||
end
|
||||
|
||||
defp social_icon(assigns) do
|
||||
~H"""
|
||||
<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>
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a cart item row.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `item` - Required. Map with `product` (containing `image_url`, `name`, `price`), `variant`, and `quantity`.
|
||||
* `currency` - Optional. Currency symbol. Defaults to "£".
|
||||
|
||||
## Examples
|
||||
|
||||
<.cart_item item={item} />
|
||||
"""
|
||||
attr :item, :map, required: true
|
||||
attr :currency, :string, default: "£"
|
||||
|
||||
def cart_item(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="flex gap-4 p-4"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<div class="w-24 h-24 flex-shrink-0 bg-gray-200 overflow-hidden" style="border-radius: var(--t-radius-image);">
|
||||
<img
|
||||
src={@item.product.image_url}
|
||||
alt={@item.product.name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold mb-1" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
||||
<%= @item.product.name %>
|
||||
</h3>
|
||||
<p class="text-sm mb-2" style="color: var(--t-text-secondary);">
|
||||
<%= @item.variant %>
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center" style="border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input);">
|
||||
<button class="px-3 py-1" style="color: var(--t-text-primary);">−</button>
|
||||
<span class="px-3 py-1 border-x" style="border-color: var(--t-border-default); color: var(--t-text-primary);">
|
||||
<%= @item.quantity %>
|
||||
</span>
|
||||
<button class="px-3 py-1" style="color: var(--t-text-primary);">+</button>
|
||||
</div>
|
||||
|
||||
<button class="text-sm" style="color: var(--t-text-tertiary);">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-lg" style="color: var(--t-text-primary);">
|
||||
<%= @currency %><%= @item.product.price / 100 * @item.quantity %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the order summary card.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `subtotal` - Required. Subtotal amount (in pence/cents).
|
||||
* `delivery` - Optional. Delivery cost. Defaults to 800 (£8.00).
|
||||
* `vat` - Optional. VAT amount. Defaults to 720 (£7.20).
|
||||
* `currency` - Optional. Currency symbol. Defaults to "£".
|
||||
* `mode` - Either `:live` (default) or `:preview`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.order_summary subtotal={3600} />
|
||||
"""
|
||||
attr :subtotal, :integer, required: true
|
||||
attr :delivery, :integer, default: 800
|
||||
attr :vat, :integer, default: 720
|
||||
attr :currency, :string, default: "£"
|
||||
attr :mode, :atom, default: :live
|
||||
|
||||
def order_summary(assigns) do
|
||||
total = assigns.subtotal + assigns.delivery + assigns.vat
|
||||
|
||||
assigns = assign(assigns, :total, total)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
class="p-6 sticky top-4"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h2 class="text-xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
||||
Order Summary
|
||||
</h2>
|
||||
|
||||
<div class="space-y-3 mb-6">
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--t-text-secondary);">Subtotal</span>
|
||||
<span style="color: var(--t-text-primary);">
|
||||
<%= @currency %><%= Float.round(@subtotal / 100, 2) %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--t-text-secondary);">Delivery</span>
|
||||
<span style="color: var(--t-text-primary);"><%= @currency %><%= Float.round(@delivery / 100, 2) %></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--t-text-secondary);">VAT (20%)</span>
|
||||
<span style="color: var(--t-text-primary);"><%= @currency %><%= Float.round(@vat / 100, 2) %></span>
|
||||
</div>
|
||||
<div class="border-t pt-3" style="border-color: var(--t-border-default);">
|
||||
<div class="flex justify-between text-lg">
|
||||
<span class="font-semibold" style="color: var(--t-text-primary);">Total</span>
|
||||
<span class="font-bold" style="color: var(--t-text-primary);">
|
||||
<%= @currency %><%= Float.round(@total / 100, 2) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="w-full px-6 py-3 font-semibold transition-all mb-3"
|
||||
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);"
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="collection"
|
||||
class="w-full px-6 py-3 font-semibold transition-all"
|
||||
style="border: 2px solid var(--t-border-default); color: var(--t-text-primary); border-radius: var(--t-radius-button); background: transparent; cursor: pointer;"
|
||||
>
|
||||
Continue Shopping
|
||||
</button>
|
||||
<% else %>
|
||||
<a
|
||||
href="/products"
|
||||
class="block w-full px-6 py-3 font-semibold transition-all text-center"
|
||||
style="border: 2px solid var(--t-border-default); color: var(--t-text-primary); border-radius: var(--t-radius-button); text-decoration: none;"
|
||||
>
|
||||
Continue Shopping
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a breadcrumb navigation.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `items` - Required. List of breadcrumb items. Each item is a map with:
|
||||
- `label` - Required. Display text
|
||||
- `page` - Optional. Page name for preview mode navigation
|
||||
- `href` - Optional. URL for live mode navigation
|
||||
- `current` - Optional. Boolean, if true this is the current page (not a link)
|
||||
* `mode` - Either `:live` (default) or `:preview`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.breadcrumb items={[
|
||||
%{label: "Home", page: "home", href: "/"},
|
||||
%{label: "Art Prints", page: "collection", href: "/products?category=art-prints"},
|
||||
%{label: "Mountain Sunrise", current: true}
|
||||
]} mode={:preview} />
|
||||
"""
|
||||
attr :items, :list, required: true
|
||||
attr :mode, :atom, default: :live
|
||||
|
||||
def breadcrumb(assigns) do
|
||||
~H"""
|
||||
<nav class="mb-8 flex items-center gap-2 text-sm" style="color: var(--t-text-secondary);">
|
||||
<%= for {item, index} <- Enum.with_index(@items) do %>
|
||||
<%= if index > 0 do %>
|
||||
<span>/</span>
|
||||
<% end %>
|
||||
<%= if item[:current] do %>
|
||||
<span style="color: var(--t-text-primary);"><%= item.label %></span>
|
||||
<% else %>
|
||||
<%= if @mode == :preview do %>
|
||||
<a href="#" phx-click="change_preview_page" phx-value-page={item.page} class="hover:underline"><%= item.label %></a>
|
||||
<% else %>
|
||||
<a href={item.href || "/"} class="hover:underline"><%= item.label %></a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</nav>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a related products section with title and product grid.
|
||||
|
||||
## Attributes
|
||||
|
||||
* `title` - Optional. Section heading. Defaults to "You might also like".
|
||||
* `products` - Required. List of products to display.
|
||||
* `theme_settings` - Required. The theme settings map.
|
||||
* `limit` - Optional. Maximum products to show. Defaults to 4.
|
||||
* `mode` - Either `:live` (default) or `:preview`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.related_products_section
|
||||
products={@related_products}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
/>
|
||||
"""
|
||||
attr :title, :string, default: "You might also like"
|
||||
attr :products, :list, required: true
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :limit, :integer, default: 4
|
||||
attr :mode, :atom, default: :live
|
||||
|
||||
def related_products_section(assigns) do
|
||||
~H"""
|
||||
<div class="py-12" style="border-top: 1px solid var(--t-border-default);">
|
||||
<h2 class="text-2xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
|
||||
<%= @title %>
|
||||
</h2>
|
||||
|
||||
<.product_grid columns={:fixed_4} gap="gap-6">
|
||||
<%= for product <- Enum.take(@products, @limit) do %>
|
||||
<.product_card
|
||||
product={product}
|
||||
theme_settings={@theme_settings}
|
||||
mode={@mode}
|
||||
variant={:compact}
|
||||
/>
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@ -20,46 +20,37 @@
|
||||
/>
|
||||
|
||||
<!-- Content Body -->
|
||||
<div class="content-body" style="padding: var(--space-xl) var(--space-lg); max-width: 800px; margin: 0 auto;">
|
||||
<!-- About Image -->
|
||||
<div
|
||||
class="content-image about-image"
|
||||
style="width: 100%; height: 300px; border-radius: var(--t-radius-image); margin-bottom: var(--space-lg); background-size: cover; background-position: center; background-image: url('/mockups/night-sky-blanket-3.jpg');"
|
||||
></div>
|
||||
<.content_body image_url="/mockups/night-sky-blanket-3.jpg">
|
||||
<p class="lead-text text-lg mb-4" style="color: var(--t-text-primary);">
|
||||
I'm Emma, a nature photographer based in the UK. What started as weekend walks with my camera has grown into something I never expected – a little shop where I can share my favourite captures with others.
|
||||
</p>
|
||||
|
||||
<!-- Content Text -->
|
||||
<div class="content-text" style="line-height: 1.7;">
|
||||
<p class="lead-text text-lg mb-4" style="color: var(--t-text-primary);">
|
||||
I'm Emma, a nature photographer based in the UK. What started as weekend walks with my camera has grown into something I never expected – a little shop where I can share my favourite captures with others.
|
||||
</p>
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
Every design in this shop comes from my own photography. Whether it's early morning mist over the hills, autumn leaves in the local woods, or the quiet beauty of wildflower meadows, I'm drawn to the peaceful moments that nature offers.
|
||||
</p>
|
||||
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
Every design in this shop comes from my own photography. Whether it's early morning mist over the hills, autumn leaves in the local woods, or the quiet beauty of wildflower meadows, I'm drawn to the peaceful moments that nature offers.
|
||||
</p>
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
I work with quality print partners to bring these images to life on products you can actually use and enjoy – from art prints for your walls to mugs for your morning tea.
|
||||
</p>
|
||||
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
I work with quality print partners to bring these images to life on products you can actually use and enjoy – from art prints for your walls to mugs for your morning tea.
|
||||
</p>
|
||||
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--t-text-xl); color: var(--t-text-primary);">
|
||||
Quality you can trust
|
||||
</h2>
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
I've carefully chosen print partners who share my commitment to quality. Every product is made to order using premium materials and printing techniques that ensure vibrant colours and lasting quality.
|
||||
</p>
|
||||
|
||||
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--t-text-xl); color: var(--t-text-primary);">
|
||||
Quality you can trust
|
||||
</h2>
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
I've carefully chosen print partners who share my commitment to quality. Every product is made to order using premium materials and printing techniques that ensure vibrant colours and lasting quality.
|
||||
</p>
|
||||
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--t-text-xl); color: var(--t-text-primary);">
|
||||
Printed sustainably
|
||||
</h2>
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
Because each item is printed on demand, there's no waste from unsold stock. My print partners use eco-friendly inks where possible, and products are shipped directly to you to minimise unnecessary handling.
|
||||
</p>
|
||||
|
||||
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--t-text-xl); color: var(--t-text-primary);">
|
||||
Printed sustainably
|
||||
</h2>
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
Because each item is printed on demand, there's no waste from unsold stock. My print partners use eco-friendly inks where possible, and products are shipped directly to you to minimise unnecessary handling.
|
||||
</p>
|
||||
|
||||
<p class="mt-8" style="color: var(--t-text-secondary);">
|
||||
Thank you for visiting. It means a lot that you're here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-8" style="color: var(--t-text-secondary);">
|
||||
Thank you for visiting. It means a lot that you're here.
|
||||
</p>
|
||||
</.content_body>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
<%
|
||||
subtotal = Enum.reduce(@preview_data.cart_items, 0, fn item, acc -> acc + item.product.price * item.quantity end)
|
||||
%>
|
||||
<div class="shop-container min-h-screen" style="position: relative; background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<!-- Skip Link for Accessibility -->
|
||||
<.skip_link />
|
||||
@ -20,100 +23,14 @@
|
||||
<div class="lg:col-span-2">
|
||||
<div class="space-y-4">
|
||||
<%= for item <- @preview_data.cart_items do %>
|
||||
<div
|
||||
class="flex gap-4 p-4"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<div class="w-24 h-24 flex-shrink-0 bg-gray-200 overflow-hidden" style="border-radius: var(--t-radius-image);">
|
||||
<img
|
||||
src={item.product.image_url}
|
||||
alt={item.product.name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold mb-1" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
||||
<%= item.product.name %>
|
||||
</h3>
|
||||
<p class="text-sm mb-2" style="color: var(--t-text-secondary);">
|
||||
<%= item.variant %>
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center" style="border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input);">
|
||||
<button class="px-3 py-1" style="color: var(--t-text-primary);">−</button>
|
||||
<span class="px-3 py-1 border-x" style="border-color: var(--t-border-default); color: var(--t-text-primary);">
|
||||
<%= item.quantity %>
|
||||
</span>
|
||||
<button class="px-3 py-1" style="color: var(--t-text-primary);">+</button>
|
||||
</div>
|
||||
|
||||
<button class="text-sm" style="color: var(--t-text-tertiary);">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-lg" style="color: var(--t-text-primary);">
|
||||
$<%= item.product.price / 100 * item.quantity %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<.cart_item item={item} currency="$" />
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Summary -->
|
||||
<div>
|
||||
<div
|
||||
class="p-6 sticky top-4"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h2 class="text-xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
||||
Order Summary
|
||||
</h2>
|
||||
|
||||
<div class="space-y-3 mb-6">
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--t-text-secondary);">Subtotal</span>
|
||||
<span style="color: var(--t-text-primary);">
|
||||
$<%= Enum.reduce(@preview_data.cart_items, 0, fn item, acc -> acc + item.product.price * item.quantity end) / 100 %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--t-text-secondary);">Delivery</span>
|
||||
<span style="color: var(--t-text-primary);">£8.00</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--t-text-secondary);">VAT (20%)</span>
|
||||
<span style="color: var(--t-text-primary);">£7.20</span>
|
||||
</div>
|
||||
<div class="border-t pt-3" style="border-color: var(--t-border-default);">
|
||||
<div class="flex justify-between text-lg">
|
||||
<span class="font-semibold" style="color: var(--t-text-primary);">Total</span>
|
||||
<span class="font-bold" style="color: var(--t-text-primary);">
|
||||
£<%= (Enum.reduce(@preview_data.cart_items, 0, fn item, acc -> acc + item.product.price * item.quantity end) / 100 + 8.00 + 7.20) |> Float.round(2) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="w-full px-6 py-3 font-semibold transition-all mb-3"
|
||||
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);"
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full px-6 py-3 font-semibold transition-all"
|
||||
style="border: 2px solid var(--t-border-default); color: var(--t-text-primary); border-radius: var(--t-radius-button);"
|
||||
>
|
||||
Continue Shopping
|
||||
</button>
|
||||
</div>
|
||||
<.order_summary subtotal={subtotal} mode={:preview} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -12,44 +12,11 @@
|
||||
|
||||
<!-- Page Header -->
|
||||
<main id="main-content">
|
||||
<div class="border-b" style="background-color: var(--t-surface-raised); border-color: var(--t-border-default);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">
|
||||
All Products
|
||||
</h1>
|
||||
<p style="color: var(--t-text-secondary);">
|
||||
<%= length(@preview_data.products) %> products
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<.collection_header title="All Products" product_count={length(@preview_data.products)} />
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Filter Bar: Category Pills + Sort -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<!-- Category Pills -->
|
||||
<div class="filter-pills-container flex gap-2 overflow-x-auto">
|
||||
<button class="filter-pill filter-pill-active">
|
||||
All
|
||||
</button>
|
||||
<%= for category <- @preview_data.categories do %>
|
||||
<button class="filter-pill">
|
||||
<%= category.name %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Sort Dropdown -->
|
||||
<select
|
||||
class="px-4 py-2"
|
||||
style="background-color: var(--t-surface-raised); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input);"
|
||||
>
|
||||
<option>Sort by: Featured</option>
|
||||
<option>Price: Low to High</option>
|
||||
<option>Price: High to Low</option>
|
||||
<option>Newest</option>
|
||||
<option>Best Selling</option>
|
||||
</select>
|
||||
</div>
|
||||
<.filter_bar categories={@preview_data.categories} />
|
||||
|
||||
<!-- Product Grid -->
|
||||
<.product_grid theme_settings={@theme_settings}>
|
||||
|
||||
@ -19,166 +19,25 @@
|
||||
|
||||
<div class="grid gap-8 md:grid-cols-2 mb-12">
|
||||
<!-- Contact Form -->
|
||||
<div
|
||||
class="p-8"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h2 class="text-xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
||||
Send us a message
|
||||
</h2>
|
||||
|
||||
<form class="space-y-4">
|
||||
<div>
|
||||
<label class="block font-medium mb-2" style="color: var(--t-text-primary);">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Your name"
|
||||
class="w-full px-4 py-2"
|
||||
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);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-medium mb-2" style="color: var(--t-text-primary);">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
class="w-full px-4 py-2"
|
||||
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);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-medium mb-2" style="color: var(--t-text-primary);">
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="How can we help?"
|
||||
class="w-full px-4 py-2"
|
||||
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);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-medium mb-2" style="color: var(--t-text-primary);">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
rows="5"
|
||||
placeholder="Your message..."
|
||||
class="w-full px-4 py-2"
|
||||
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);"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full px-6 py-3 font-semibold transition-all"
|
||||
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);"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<.contact_form />
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="space-y-6">
|
||||
<!-- Order Tracking -->
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Track your order</h3>
|
||||
<p class="text-sm mb-3" style="color: var(--t-text-secondary);">
|
||||
Enter your email and we'll send you a link to check your order status.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
class="flex-1 min-w-0 px-3 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
|
||||
class="px-4 py-2 text-sm font-medium 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);"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<.order_tracking_card />
|
||||
|
||||
<!-- Helpful info -->
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Handy to know</h3>
|
||||
<ul class="space-y-2 text-sm" style="color: var(--t-text-secondary);">
|
||||
<li class="flex items-start gap-2">
|
||||
<span style="color: var(--t-text-tertiary);">•</span>
|
||||
<span><strong style="color: var(--t-text-primary);">Printing:</strong> 2-5 business days</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span style="color: var(--t-text-tertiary);">•</span>
|
||||
<span><strong style="color: var(--t-text-primary);">Delivery:</strong> 3-7 business days after printing</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span style="color: var(--t-text-tertiary);">•</span>
|
||||
<span><strong style="color: var(--t-text-primary);">Returns:</strong> Happy to help with faulty or damaged items</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<.info_card title="Handy to know" items={[
|
||||
%{label: "Printing", value: "2-5 business days"},
|
||||
%{label: "Delivery", value: "3-7 business days after printing"},
|
||||
%{label: "Returns", value: "Happy to help with faulty or damaged items"}
|
||||
]} />
|
||||
|
||||
<!-- Get in touch -->
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Get in touch</h3>
|
||||
<a href="mailto:hello@example.com" class="flex items-center gap-2 mb-2" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); text-decoration: none;">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
hello@example.com
|
||||
</a>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||
We typically respond within 24 hours
|
||||
</p>
|
||||
</div>
|
||||
<.contact_info_card email="hello@example.com" />
|
||||
|
||||
<!-- Social links -->
|
||||
<div class="flex gap-4 justify-center">
|
||||
<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>
|
||||
<.social_links />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -21,75 +21,25 @@
|
||||
/>
|
||||
|
||||
<!-- Categories -->
|
||||
<section style="padding: var(--space-xl) var(--space-lg); background-color: var(--t-surface-base);">
|
||||
<nav class="grid grid-cols-3 gap-4 max-w-3xl mx-auto" aria-label="Product categories">
|
||||
<%= for category <- Enum.take(@preview_data.categories, 3) do %>
|
||||
<a href="#" phx-click="change_preview_page" phx-value-page="collection" class="flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5" style="text-decoration: none; cursor: pointer;">
|
||||
<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}');"}
|
||||
></div>
|
||||
<span class="text-sm font-medium" style="font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<%= category.name %>
|
||||
</span>
|
||||
</a>
|
||||
<% end %>
|
||||
</nav>
|
||||
</section>
|
||||
<.category_nav categories={@preview_data.categories} mode={:preview} />
|
||||
|
||||
<!-- Featured Products -->
|
||||
<section style="padding: var(--space-xl) var(--space-lg); background-color: var(--t-surface-sunken);">
|
||||
<h2 class="text-2xl mb-6" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking); color: var(--t-text-primary);">
|
||||
Featured products
|
||||
</h2>
|
||||
|
||||
<.product_grid theme_settings={@theme_settings}>
|
||||
<%= for product <- Enum.take(@preview_data.products, 4) do %>
|
||||
<.product_card
|
||||
product={product}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
variant={:featured}
|
||||
/>
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
|
||||
<div class="text-center mt-8">
|
||||
<button
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="collection"
|
||||
class="px-6 py-3 font-medium transition-all"
|
||||
style="background-color: transparent; color: var(--t-text-primary); border: 1px solid var(--t-text-primary); border-radius: var(--t-radius-button); cursor: pointer;"
|
||||
>
|
||||
View all products
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<.featured_products_section
|
||||
title="Featured products"
|
||||
products={@preview_data.products}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
/>
|
||||
|
||||
<!-- About Section -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center" style="padding: var(--space-2xl) var(--space-lg); background-color: var(--t-surface-base);">
|
||||
<div
|
||||
class="h-72 rounded-lg bg-cover bg-center"
|
||||
style="background-image: url('/mockups/mountain-sunrise-print-3.jpg'); border-radius: var(--t-radius-image);"
|
||||
></div>
|
||||
<div>
|
||||
<h2 class="text-2xl mb-4" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking); color: var(--t-text-primary);">
|
||||
Made with passion, printed with care
|
||||
</h2>
|
||||
<p class="text-base mb-4" style="color: var(--t-text-secondary); line-height: 1.7;">
|
||||
Every design starts with an idea. We work with quality print partners to bring those ideas to life on premium products – from gallery-quality art prints to everyday essentials.
|
||||
</p>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="about"
|
||||
class="text-sm font-medium transition-colors"
|
||||
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); text-decoration: none; cursor: pointer;"
|
||||
>
|
||||
Learn more about the studio →
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<.image_text_section
|
||||
title="Made with passion, printed with care"
|
||||
description="Every design starts with an idea. We work with quality print partners to bring those ideas to life on premium products – from gallery-quality art prints to everyday essentials."
|
||||
image_url="/mockups/mountain-sunrise-print-3.jpg"
|
||||
link_text="Learn more about the studio →"
|
||||
link_page="about"
|
||||
mode={:preview}
|
||||
/>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@ -15,13 +15,11 @@
|
||||
|
||||
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-8 flex items-center gap-2 text-sm" style="color: var(--t-text-secondary);">
|
||||
<a href="#" phx-click="change_preview_page" phx-value-page="home" class="hover:underline">Home</a>
|
||||
<span>/</span>
|
||||
<a href="#" phx-click="change_preview_page" phx-value-page="collection" class="hover:underline"><%= product.category %></a>
|
||||
<span>/</span>
|
||||
<span style="color: var(--t-text-primary);"><%= product.name %></span>
|
||||
</nav>
|
||||
<.breadcrumb items={[
|
||||
%{label: "Home", page: "home", href: "/"},
|
||||
%{label: product.category, page: "collection", href: "/products"},
|
||||
%{label: product.name, current: true}
|
||||
]} mode={:preview} />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16">
|
||||
<!-- Product Images -->
|
||||
@ -397,22 +395,11 @@
|
||||
|
||||
<!-- Related Products -->
|
||||
<%= if @theme_settings.pdp_related_products do %>
|
||||
<div class="py-12" style="border-top: 1px solid var(--t-border-default);">
|
||||
<h2 class="text-2xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
|
||||
You might also like
|
||||
</h2>
|
||||
|
||||
<.product_grid columns={:fixed_4} gap="gap-6">
|
||||
<%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %>
|
||||
<.product_card
|
||||
product={related_product}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
variant={:compact}
|
||||
/>
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
</div>
|
||||
<.related_products_section
|
||||
products={Enum.slice(@preview_data.products, 1, 4)}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
/>
|
||||
<% end %>
|
||||
</main>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user