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,15 +20,7 @@
|
||||
/>
|
||||
|
||||
<!-- 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 Text -->
|
||||
<div class="content-text" style="line-height: 1.7;">
|
||||
<.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>
|
||||
@ -58,8 +50,7 @@
|
||||
<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>
|
||||
</.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}
|
||||
<.featured_products_section
|
||||
title="Featured products"
|
||||
products={@preview_data.products}
|
||||
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>
|
||||
|
||||
<!-- 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,23 +395,12 @@
|
||||
|
||||
<!-- 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}
|
||||
<.related_products_section
|
||||
products={Enum.slice(@preview_data.products, 1, 4)}
|
||||
theme_settings={@theme_settings}
|
||||
mode={:preview}
|
||||
variant={:compact}
|
||||
/>
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
</div>
|
||||
<% end %>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
Loading…
Reference in New Issue
Block a user