feat: add preview page templates with theme styling

Implement all 7 preview pages showcasing theme customization:
- Home page: hero, featured products, testimonials, categories
- Collection page: product grid with filters and sorting
- Product detail page (PDP): gallery, variants, add to cart
- Cart page: cart items with quantity controls and order summary
- About page: company story and values
- Contact page: contact form and business information
- 404 error page: error message with product suggestions

Features:
- All pages use CSS custom properties for theming
- Preview data from PreviewData module (mock products, testimonials, categories)
- Responsive layouts with Tailwind utilities
- Grid columns respect theme settings
- Colors, typography, shapes, and spacing all theme-aware
- Components created as embed_templates for clean separation

Technical implementation:
- Created PreviewPages component module with embed_templates
- Wired up preview_data in LiveView mount
- Updated index.html.heex to render preview pages based on @preview_page
- All pages styled with inline styles using CSS variables
- Scrollable preview frame with max-height

All tests passing (197 total).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jamey Greenwood 2025-12-30 22:06:04 +00:00
parent da770f121f
commit 6a3069f854
11 changed files with 848 additions and 48 deletions

View File

@ -2,12 +2,18 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
use SimpleshopThemeWeb, :live_view
alias SimpleshopTheme.Settings
alias SimpleshopTheme.Theme.{CSSGenerator, Presets}
alias SimpleshopTheme.Theme.{CSSGenerator, Presets, PreviewData}
@impl true
def mount(_params, _session, socket) do
theme_settings = Settings.get_theme_settings()
generated_css = CSSGenerator.generate(theme_settings)
preview_data = %{
products: PreviewData.products(),
cart_items: PreviewData.cart_items(),
testimonials: PreviewData.testimonials(),
categories: PreviewData.categories()
}
socket =
socket
@ -15,6 +21,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
|> assign(:generated_css, generated_css)
|> assign(:preview_page, :home)
|> assign(:preset_names, Presets.list_names())
|> assign(:preview_data, preview_data)
{:ok, socket}
end

View File

@ -103,56 +103,27 @@
</div>
<!-- Preview Frame -->
<div class="preview-frame bg-white" style="min-height: 600px;">
<div class="preview-frame bg-white overflow-auto" style="min-height: 600px; max-height: calc(100vh - 200px);">
<style>
<%= Phoenix.HTML.raw(@generated_css) %>
</style>
<div class="p-8 text-center">
<h2 class="text-2xl font-bold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
Preview: <%= String.capitalize(to_string(@preview_page)) %>
</h2>
<div class="max-w-2xl mx-auto space-y-4">
<p style="color: var(--t-text-secondary);">
This is a preview of the <strong><%= @preview_page %></strong> page with your current theme settings.
</p>
<div class="flex gap-2 justify-center">
<button
class="px-4 py-2 rounded text-white font-medium"
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); border-radius: var(--t-radius-button);"
>
Primary Button
</button>
<button
class="px-4 py-2 rounded font-medium"
style="border: 2px solid var(--t-border-default); border-radius: var(--t-radius-button); color: var(--t-text-primary); background-color: var(--t-surface-base);"
>
Secondary Button
</button>
</div>
<div
class="p-6 rounded shadow-sm"
style="background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); border: 1px solid var(--t-border-default);"
>
<h3
class="text-lg font-semibold mb-2"
style="font-family: var(--t-font-heading); color: var(--t-text-primary);"
>
Card Example
</h3>
<p style="font-family: var(--t-font-body); color: var(--t-text-secondary);">
This card demonstrates the current surface, border, and text colors with the selected shape style.
</p>
</div>
<div class="text-sm" style="color: var(--t-text-tertiary);">
Detailed preview pages coming in Phase 5
</div>
</div>
</div>
<%= case @preview_page do %>
<% :home -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.home preview_data={@preview_data} theme_settings={@theme_settings} />
<% :collection -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.collection preview_data={@preview_data} theme_settings={@theme_settings} />
<% :pdp -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.pdp preview_data={@preview_data} theme_settings={@theme_settings} />
<% :cart -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.cart preview_data={@preview_data} theme_settings={@theme_settings} />
<% :about -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.about preview_data={@preview_data} theme_settings={@theme_settings} />
<% :contact -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.contact preview_data={@preview_data} theme_settings={@theme_settings} />
<% :error -> %>
<SimpleshopThemeWeb.ThemeLive.PreviewPages.error preview_data={@preview_data} theme_settings={@theme_settings} />
<% end %>
</div>
</div>
</div>

View File

@ -0,0 +1,5 @@
defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
use Phoenix.Component
embed_templates "preview_pages/*"
end

View File

@ -0,0 +1,67 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<h1 class="text-4xl md:text-5xl font-bold mb-6 text-center" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">
About Us
</h1>
<p class="text-lg mb-12 text-center max-w-2xl mx-auto" style="color: var(--t-text-secondary);">
We're passionate about bringing you the finest products, handpicked with care and attention to detail.
</p>
<div class="aspect-video bg-gray-200 mb-12 overflow-hidden" style="border-radius: var(--t-radius-card);">
<img
src="https://placehold.co/1200x675/e5e5e5/525252?text=Our+Story"
alt="Our Story"
class="w-full h-full object-cover"
/>
</div>
<div class="prose max-w-none mb-16">
<h2 class="text-2xl font-bold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
Our Story
</h2>
<p class="mb-4 leading-relaxed" style="color: var(--t-text-secondary);">
Founded in 2020, our journey began with a simple mission: to curate and deliver exceptional products that enhance everyday life. We believe that quality shouldn't be compromised, and that's why every item in our collection is carefully selected for its craftsmanship, sustainability, and timeless appeal.
</p>
<p class="mb-4 leading-relaxed" style="color: var(--t-text-secondary);">
What started as a small passion project has grown into a community of like-minded individuals who appreciate the finer things in life. We work directly with artisans and makers who share our values of quality, authenticity, and ethical production.
</p>
<h2 class="text-2xl font-bold mb-4 mt-8" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
Our Values
</h2>
<div class="grid gap-6 md:grid-cols-3 mb-8">
<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-2" style="color: var(--t-text-primary);">Quality First</h3>
<p class="text-sm" style="color: var(--t-text-secondary);">
Every product is vetted for exceptional quality and durability.
</p>
</div>
<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-2" style="color: var(--t-text-primary);">Sustainability</h3>
<p class="text-sm" style="color: var(--t-text-secondary);">
We prioritize eco-friendly materials and ethical production methods.
</p>
</div>
<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-2" style="color: var(--t-text-primary);">Community</h3>
<p class="text-sm" style="color: var(--t-text-secondary);">
Supporting local artisans and building lasting relationships.
</p>
</div>
</div>
</div>
<div class="text-center">
<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);">
Ready to Explore?
</h2>
<button
class="px-8 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);"
>
Shop Our Collection
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,109 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<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-8" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">
Shopping Cart
</h1>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Cart Items -->
<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>
<% 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);">Shipping</span>
<span style="color: var(--t-text-primary);">$10.00</span>
</div>
<div class="flex justify-between">
<span style="color: var(--t-text-secondary);">Tax</span>
<span style="color: var(--t-text-primary);">$8.50</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 + 10.00 + 8.50) |> 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>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,159 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Header -->
<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>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex flex-col md:flex-row gap-8">
<!-- Filters Sidebar -->
<div class="w-full md:w-64 flex-shrink-0">
<div class="p-4" style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);">
<h3 class="font-semibold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
Filter by Category
</h3>
<div class="space-y-2">
<%= for category <- @preview_data.categories do %>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); border-radius: var(--t-radius-input);"
/>
<span style="color: var(--t-text-primary);"><%= category.name %></span>
<span class="ml-auto text-sm" style="color: var(--t-text-tertiary);">
<%= category.product_count %>
</span>
</label>
<% end %>
</div>
<div class="mt-6 pt-6" style="border-top: 1px solid var(--t-border-subtle);">
<h3 class="font-semibold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
Price Range
</h3>
<div class="space-y-2">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
/>
<span style="color: var(--t-text-primary);">Under $25</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
/>
<span style="color: var(--t-text-primary);">$25 - $50</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
/>
<span style="color: var(--t-text-primary);">$50 - $100</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
/>
<span style="color: var(--t-text-primary);">Over $100</span>
</label>
</div>
</div>
</div>
</div>
<!-- Product Grid -->
<div class="flex-1">
<div class="flex items-center justify-between mb-6">
<p style="color: var(--t-text-secondary);">
Showing all <%= length(@preview_data.products) %> products
</p>
<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>
<div class={"grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-#{@theme_settings.grid_columns || "3"}"}>
<%= for product <- @preview_data.products do %>
<div
class="group overflow-hidden transition-all"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<div class="aspect-square bg-gray-200 overflow-hidden relative">
<%= if product.on_sale do %>
<div class="absolute top-2 right-2 px-2 py-1 text-xs font-bold text-white rounded" style="background-color: var(--t-sale-color);">
SALE
</div>
<% end %>
<%= if not product.in_stock do %>
<div class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<span class="text-white font-semibold">Out of Stock</span>
</div>
<% end %>
<img
src={product.image_url}
alt={product.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div class="p-4">
<p class="text-xs mb-1" style="color: var(--t-text-tertiary);">
<%= product.category %>
</p>
<h3 class="font-semibold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= product.name %>
</h3>
<div class="flex items-center justify-between">
<div>
<%= if product.on_sale do %>
<span class="text-lg font-bold" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
$<%= product.price / 100 %>
</span>
<span class="text-sm line-through ml-2" style="color: var(--t-text-tertiary);">
$<%= product.compare_at_price / 100 %>
</span>
<% else %>
<span class="text-lg font-bold" style="color: var(--t-text-primary);">
$<%= product.price / 100 %>
</span>
<% end %>
</div>
<%= if product.in_stock do %>
<button
class="px-3 py-1.5 text-sm font-medium 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);"
>
Add
</button>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,124 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<h1 class="text-4xl md:text-5xl font-bold mb-6 text-center" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">
Contact Us
</h1>
<p class="text-lg mb-12 text-center max-w-2xl mx-auto" style="color: var(--t-text-secondary);">
Have a question or comment? We'd love to hear from you. Send us a message and we'll respond as soon as possible.
</p>
<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 Info -->
<div class="space-y-6">
<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-2" style="color: var(--t-text-primary);">Email</h3>
<p style="color: var(--t-text-secondary);">hello@example.com</p>
</div>
<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-2" style="color: var(--t-text-primary);">Phone</h3>
<p style="color: var(--t-text-secondary);">(555) 123-4567</p>
</div>
<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-2" style="color: var(--t-text-primary);">Address</h3>
<p style="color: var(--t-text-secondary);">
123 Main Street<br />
San Francisco, CA 94102<br />
United States
</p>
</div>
<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-2" style="color: var(--t-text-primary);">Hours</h3>
<p style="color: var(--t-text-secondary);">
Monday - Friday: 9am - 6pm<br />
Saturday: 10am - 4pm<br />
Sunday: Closed
</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
<div class="min-h-screen flex items-center justify-center" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
<h1 class="text-8xl md:text-9xl font-bold mb-4" style="font-family: var(--t-font-heading); color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); font-weight: var(--t-heading-weight);">
404
</h1>
<h2 class="text-3xl md:text-4xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">
Page Not Found
</h2>
<p class="text-lg mb-8 max-w-md mx-auto" style="color: var(--t-text-secondary);">
Sorry, we couldn't find the page you're looking for. Perhaps you've mistyped the URL or the page has been moved.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<button
class="px-8 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);"
>
Go to Homepage
</button>
<button
class="px-8 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);"
>
Browse Products
</button>
</div>
<div class="mt-12 grid gap-4 grid-cols-2 md:grid-cols-4 max-w-xl mx-auto">
<%= for product <- Enum.take(@preview_data.products, 4) do %>
<div
class="group overflow-hidden"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);"
>
<div class="aspect-square bg-gray-200 overflow-hidden">
<img
src={product.image_url}
alt={product.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div class="p-2">
<p class="text-xs font-semibold truncate" style="color: var(--t-text-primary);">
<%= product.name %>
</p>
<p class="text-xs" style="color: var(--t-text-secondary);">
$<%= product.price / 100 %>
</p>
</div>
</div>
<% end %>
</div>
</div>
</div>

View File

@ -0,0 +1,134 @@
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<!-- Hero Section -->
<div class="relative" style="background-color: var(--t-surface-raised);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 md:py-24">
<div class="text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">
Welcome to Our Store
</h1>
<p class="text-lg md:text-xl mb-8 max-w-2xl mx-auto" style="color: var(--t-text-secondary);">
Discover our curated collection of handpicked products crafted with care
</p>
<button
class="px-8 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);"
>
Shop Now
</button>
</div>
</div>
</div>
<!-- Featured Products -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<h2 class="text-3xl font-bold mb-8" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
Featured Products
</h2>
<div class={"grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-#{@theme_settings.grid_columns || "3"}"}>
<%= for product <- Enum.take(@preview_data.products, 6) do %>
<div
class="group overflow-hidden transition-all"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<div class="aspect-square bg-gray-200 overflow-hidden">
<img
src={product.image_url}
alt={product.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div class="p-4">
<h3 class="font-semibold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= product.name %>
</h3>
<p class="text-sm mb-3" style="color: var(--t-text-secondary);">
<%= product.description %>
</p>
<div class="flex items-center justify-between">
<div>
<%= if product.on_sale do %>
<span class="text-lg font-bold" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
$<%= product.price / 100 %>
</span>
<span class="text-sm line-through ml-2" style="color: var(--t-text-tertiary);">
$<%= product.compare_at_price / 100 %>
</span>
<% else %>
<span class="text-lg font-bold" style="color: var(--t-text-primary);">
$<%= product.price / 100 %>
</span>
<% end %>
</div>
<button
class="px-4 py-2 text-sm font-medium 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);"
>
Add to Cart
</button>
</div>
</div>
</div>
<% end %>
</div>
</div>
<!-- Testimonials -->
<div class="py-16" style="background-color: var(--t-surface-sunken);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold mb-12 text-center" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
What Our Customers Say
</h2>
<div class="grid gap-6 grid-cols-1 md:grid-cols-3">
<%= for testimonial <- Enum.take(@preview_data.testimonials, 3) do %>
<div class="p-6" style="background-color: var(--t-surface-base); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);">
<div class="flex mb-3">
<%= for _ <- 1..testimonial.rating do %>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
<% end %>
</div>
<p class="mb-4" style="color: var(--t-text-primary);">
"<%= testimonial.content %>"
</p>
<p class="font-semibold" style="color: var(--t-text-secondary);">
— <%= testimonial.author %>
</p>
</div>
<% end %>
</div>
</div>
</div>
<!-- Categories -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<h2 class="text-3xl font-bold mb-8" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
Shop by Category
</h2>
<div class="grid gap-6 grid-cols-2 md:grid-cols-3 lg:grid-cols-5">
<%= for category <- @preview_data.categories do %>
<div
class="group cursor-pointer overflow-hidden"
style="border-radius: var(--t-radius-card);"
>
<div class="aspect-square bg-gray-200 overflow-hidden mb-3">
<img
src={category.image_url}
alt={category.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<h3 class="font-semibold text-center" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= category.name %>
</h3>
<p class="text-sm text-center" style="color: var(--t-text-tertiary);">
<%= category.product_count %> products
</p>
</div>
<% end %>
</div>
</div>
</div>

View File

@ -0,0 +1,168 @@
<%
product = List.first(@preview_data.products)
%>
<div class="min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<div class="mb-8 flex items-center gap-2 text-sm" style="color: var(--t-text-secondary);">
<span>Home</span>
<span>/</span>
<span><%= product.category %></span>
<span>/</span>
<span style="color: var(--t-text-primary);"><%= product.name %></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16">
<!-- Product Images -->
<div>
<div class="aspect-square bg-gray-200 mb-4 overflow-hidden" style="border-radius: var(--t-radius-image);">
<img
src={product.image_url}
alt={product.name}
class="w-full h-full object-cover"
/>
</div>
<div class="grid grid-cols-4 gap-4">
<%= for img_url <- [product.image_url, product.hover_image_url, product.image_url, product.hover_image_url] do %>
<div class="aspect-square bg-gray-200 cursor-pointer overflow-hidden" style="border: 2px solid var(--t-border-default); border-radius: var(--t-radius-image);">
<img
src={img_url}
alt={product.name}
class="w-full h-full object-cover"
/>
</div>
<% end %>
</div>
</div>
<!-- Product Info -->
<div>
<h1 class="text-3xl md:text-4xl font-bold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);">
<%= product.name %>
</h1>
<div class="flex items-center gap-4 mb-6">
<%= if product.on_sale do %>
<span class="text-3xl font-bold" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
$<%= product.price / 100 %>
</span>
<span class="text-xl line-through" style="color: var(--t-text-tertiary);">
$<%= product.compare_at_price / 100 %>
</span>
<span class="px-2 py-1 text-sm font-bold text-white rounded" style="background-color: var(--t-sale-color);">
SAVE <%= round((product.compare_at_price - product.price) / product.compare_at_price * 100) %>%
</span>
<% else %>
<span class="text-3xl font-bold" style="color: var(--t-text-primary);">
$<%= product.price / 100 %>
</span>
<% end %>
</div>
<p class="mb-8 leading-relaxed" style="color: var(--t-text-secondary);">
<%= product.description %>. Crafted with attention to detail and quality materials, this product is designed to last. Perfect for everyday use or special occasions.
</p>
<!-- Variant Options -->
<div class="mb-6">
<label class="block font-semibold mb-2" style="color: var(--t-text-primary);">
Size
</label>
<div class="flex gap-2">
<%= for size <- ["S", "M", "L", "XL"] do %>
<button
class="px-4 py-2 font-medium transition-all"
style="border: 2px solid var(--t-border-default); border-radius: var(--t-radius-button); color: var(--t-text-primary);"
>
<%= size %>
</button>
<% end %>
</div>
</div>
<!-- Quantity -->
<div class="mb-8">
<label class="block font-semibold mb-2" style="color: var(--t-text-primary);">
Quantity
</label>
<div class="flex items-center gap-4">
<div class="flex items-center" style="border: 2px solid var(--t-border-default); border-radius: var(--t-radius-input);">
<button class="px-4 py-2" style="color: var(--t-text-primary);"></button>
<span class="px-4 py-2 border-x-2" style="border-color: var(--t-border-default); color: var(--t-text-primary);">
1
</span>
<button class="px-4 py-2" style="color: var(--t-text-primary);">+</button>
</div>
<%= if product.in_stock do %>
<span class="text-sm" style="color: var(--t-text-tertiary);">In stock</span>
<% else %>
<span class="text-sm font-semibold" style="color: var(--t-sale-color);">Out of stock</span>
<% end %>
</div>
</div>
<!-- Add to Cart -->
<button
class="w-full px-6 py-4 text-lg font-semibold transition-all mb-4"
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);"
>
Add to Cart
</button>
<!-- Features -->
<div class="p-4 space-y-3" style="background-color: var(--t-surface-sunken); border-radius: var(--t-radius-card);">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<div>
<p class="font-semibold" style="color: var(--t-text-primary);">Free Shipping</p>
<p class="text-sm" style="color: var(--t-text-secondary);">On orders over $50</p>
</div>
</div>
<div class="flex items-start gap-3">
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p class="font-semibold" style="color: var(--t-text-primary);">Easy Returns</p>
<p class="text-sm" style="color: var(--t-text-secondary);">30-day return policy</p>
</div>
</div>
</div>
</div>
</div>
<!-- Related Products -->
<div>
<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 May Also Like
</h2>
<div class="grid gap-6 grid-cols-2 md:grid-cols-4">
<%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %>
<div
class="group overflow-hidden"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<div class="aspect-square bg-gray-200 overflow-hidden">
<img
src={related_product.image_url}
alt={related_product.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div class="p-3">
<h3 class="font-semibold text-sm mb-1" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= related_product.name %>
</h3>
<p class="font-bold" style="color: var(--t-text-primary);">
$<%= related_product.price / 100 %>
</p>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>

View File

@ -90,7 +90,7 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|> element("button", "Collection")
|> render_click()
assert html =~ "Preview: Collection"
assert html =~ "All Products"
end
test "save theme button works", %{conn: conn} do