feat: redesign contact page for POD sellers & add theme enhancements

Contact page redesign:
- Replace retail-style contact info (phone, address, hours) with POD-appropriate layout
- Add order tracking card with email input
- Add "Handy to know" section with printing/delivery/returns info
- Add email contact with response time promise
- Add social links (Instagram, Pinterest)
- Update intro text to be warmer and more personal

Collection page improvements:
- Replace sidebar filters with horizontal category pills
- Add filter pill CSS with theme token integration

PDP enhancements:
- Add image lightbox with keyboard navigation
- Add thumbnail gallery with active state
- Add reviews section (toggleable)
- Add related products section (toggleable)
- Add trust badges section (toggleable)

Theme system additions:
- Add button_style setting (filled/outline/soft)
- Add product_text_align setting (left/center)
- Add image_aspect_ratio setting (square/portrait/landscape)
- Add responsive form layouts with flex-wrap

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jamey Greenwood 2026-01-02 13:48:03 +00:00
parent 98a9e3b3d4
commit 37653e5e7a
14 changed files with 1178 additions and 236 deletions

View File

@ -252,6 +252,89 @@
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
} }
/* =============================================
Dynamic Theme Settings (consume CSS variables)
============================================= */
/* Density - apply to product grids */
.preview-frame .product-grid {
gap: var(--space-lg, 1.5rem);
}
.preview-frame[data-density="spacious"] .product-grid {
gap: calc(var(--space-lg, 1.5rem) * 1.25);
}
.preview-frame[data-density="compact"] .product-grid {
gap: calc(var(--space-lg, 1.5rem) * 0.75);
}
/* Density also affects card padding */
.preview-frame .product-card > div:last-child {
padding: var(--space-md, 1rem);
}
.preview-frame[data-density="spacious"] .product-card > div:last-child {
padding: calc(var(--space-md, 1rem) * 1.25);
}
.preview-frame[data-density="compact"] .product-card > div:last-child {
padding: calc(var(--space-md, 1rem) * 0.75);
}
/* Product Text Alignment - targets the product info area inside cards */
.preview-frame .product-card > div:last-child {
text-align: var(--t-product-text-align, left);
}
/* Image Aspect Ratio - targets the image container inside product cards */
.preview-frame .product-card .product-image-container {
aspect-ratio: var(--t-image-aspect-ratio, 1 / 1);
}
/* Font Size Scale - applied to base font (16px is accessible minimum) */
.preview-frame {
font-size: calc(16px * var(--t-font-size-scale, 1));
}
/* Heading Weight Override - takes precedence over typography preset */
.preview-frame h1,
.preview-frame h2,
.preview-frame h3,
.preview-frame h4,
.preview-frame h5,
.preview-frame h6 {
font-weight: var(--t-heading-weight-override, var(--t-heading-weight, 600)) !important;
}
/* Layout Max Width - applied via data attribute for better specificity */
.preview-frame[data-layout="contained"] .max-w-7xl {
max-width: var(--t-layout-max-width, 1100px);
}
.preview-frame[data-layout="wide"] .max-w-7xl {
max-width: var(--t-layout-max-width, 1400px);
}
.preview-frame[data-layout="full"] .max-w-7xl {
max-width: var(--t-layout-max-width, 100%);
}
/* Button Style - using data attribute approach */
/* Outline button style */
.preview-frame[data-button-style="outline"] button[style*="background-color: hsl(var(--t-accent"] {
background-color: transparent !important;
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)) !important;
border: 2px solid hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)) !important;
}
/* Soft button style */
.preview-frame[data-button-style="soft"] button[style*="background-color: hsl(var(--t-accent"] {
background-color: hsl(var(--t-accent-h) var(--t-accent-s) 90%) !important;
color: hsl(var(--t-accent-h) var(--t-accent-s) 30%) !important;
border: 2px solid transparent !important;
}
/* Product Badges */ /* Product Badges */
.product-badge { .product-badge {
position: absolute; position: absolute;
@ -276,6 +359,11 @@
color: var(--t-text-inverse); color: var(--t-text-inverse);
} }
.badge-sold-out {
background-color: var(--t-text-tertiary, #737373);
color: #ffffff;
}
/* Product Hover Image */ /* Product Hover Image */
.product-image-container { .product-image-container {
position: relative; position: relative;
@ -292,20 +380,46 @@
opacity: 1; opacity: 1;
} }
.product-card:hover .product-image-primary { /* Only hide primary image on hover when a hover image sibling exists */
.product-card:hover .product-image-primary:has(+ .product-image-hover) {
opacity: 0; opacity: 0;
} }
/* Secondary Accent (Hover Colour) Usage */
/* Applied to interactive elements on hover for visual feedback */
/* Links in body text */
.preview-frame a:not([class*="btn"]):not([class*="button"]):not(.product-card):not(.nav-link):hover {
color: var(--t-secondary-accent, var(--t-text-primary));
}
/* Product card hover effect - subtle accent border */
.preview-frame .product-card:hover {
border-color: var(--t-secondary-accent, var(--t-border-default)) !important;
}
/* Button hover states - darken or use secondary accent */
.preview-frame button:hover,
.preview-frame [role="button"]:hover {
filter: brightness(0.95);
}
/* Nav links hover */
.preview-frame .nav-link:hover,
.preview-frame nav a:hover {
color: var(--t-secondary-accent, var(--t-text-primary));
}
/* Social Links */ /* Social Links */
.social-link:hover { .social-link:hover {
background-color: var(--t-surface-sunken); background-color: var(--t-surface-sunken);
color: var(--t-text-primary); color: var(--t-secondary-accent, var(--t-text-primary));
} }
/* Header Icon Buttons */ /* Header Icon Buttons */
.header-icon-btn:hover { .header-icon-btn:hover {
background-color: var(--t-surface-sunken); background-color: var(--t-surface-sunken);
color: var(--t-text-primary); color: var(--t-secondary-accent, var(--t-text-primary));
} }
/* Search Modal Animation */ /* Search Modal Animation */
@ -318,3 +432,225 @@
transform: translateY(0); transform: translateY(0);
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
/* Lightbox - using native dialog */
.lightbox {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
border: none;
padding: 0;
margin: 0;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
}
.lightbox::backdrop {
background: rgba(0, 0, 0, 0.95);
}
.lightbox:not([open]) {
display: none;
}
.lightbox-content {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.lightbox-close {
position: absolute;
top: var(--space-md, 1rem);
right: var(--space-md, 1rem);
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.05);
border: none;
border-radius: 50%;
color: white;
cursor: pointer;
transition: background 0.15s ease;
z-index: 1;
}
.lightbox-close:hover {
background: rgba(255, 255, 255, 0.1);
}
.lightbox-close svg {
width: 24px;
height: 24px;
}
.lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 50%;
color: white;
cursor: pointer;
transition: background 0.15s ease;
}
.lightbox-nav:hover {
background: rgba(255, 255, 255, 0.2);
}
.lightbox-nav svg {
width: 24px;
height: 24px;
}
.lightbox-prev {
left: var(--space-md, 1rem);
}
.lightbox-next {
right: var(--space-md, 1rem);
}
.lightbox-image-container {
max-width: 90vw;
max-height: 75vh;
display: flex;
align-items: center;
justify-content: center;
}
.lightbox-image {
max-width: 100%;
max-height: 75vh;
object-fit: contain;
border-radius: var(--t-radius-image, 8px);
}
.lightbox-figure {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-md, 1rem);
}
.lightbox-caption {
color: rgba(255, 255, 255, 0.8);
font-family: var(--t-font-body);
font-size: 0.875rem;
text-align: center;
max-width: 600px;
line-height: 1.5;
}
.lightbox-counter {
position: absolute;
bottom: var(--space-md, 1rem);
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.7);
font-size: 0.875rem;
font-family: var(--t-font-body);
}
/* PDP Main Image zoom cursor */
.pdp-main-image-container {
cursor: zoom-in;
}
/* =============================================
Type Scale Utility Classes
These scale with the font-size setting
============================================= */
/* Body text sizes */
.preview-frame .t-caption { font-size: var(--t-text-caption); }
.preview-frame .t-small { font-size: var(--t-text-small); }
.preview-frame .t-base { font-size: var(--t-text-base); }
.preview-frame .t-large { font-size: var(--t-text-large); }
.preview-frame .t-xl { font-size: var(--t-text-xl); }
.preview-frame .t-2xl { font-size: var(--t-text-2xl); }
/* Heading sizes */
.preview-frame .t-heading-sm { font-size: var(--t-heading-sm); }
.preview-frame .t-heading-md { font-size: var(--t-heading-md); }
.preview-frame .t-heading-lg { font-size: var(--t-heading-lg); }
.preview-frame .t-heading-xl { font-size: var(--t-heading-xl); }
.preview-frame .t-heading-display { font-size: var(--t-heading-display); }
/* Override Tailwind text-* classes within preview to use our scale */
.preview-frame .text-xs { font-size: var(--t-text-caption) !important; }
.preview-frame .text-sm { font-size: var(--t-text-small) !important; }
.preview-frame .text-base { font-size: var(--t-text-base) !important; }
.preview-frame .text-lg { font-size: var(--t-text-large) !important; }
.preview-frame .text-xl { font-size: var(--t-text-xl) !important; }
.preview-frame .text-2xl { font-size: var(--t-text-2xl) !important; }
/* Map larger Tailwind sizes to our heading scale */
.preview-frame .text-3xl { font-size: var(--t-heading-lg) !important; }
.preview-frame .text-4xl { font-size: var(--t-heading-xl) !important; }
.preview-frame .text-5xl,
.preview-frame .text-6xl,
.preview-frame .text-7xl,
.preview-frame .text-8xl,
.preview-frame .text-9xl { font-size: var(--t-heading-display) !important; }
/* =============================================
Filter Pills (Collection Page)
============================================= */
.preview-frame .filter-pills-container {
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.preview-frame .filter-pills-container::-webkit-scrollbar {
display: none;
}
.preview-frame .filter-pill {
flex-shrink: 0;
padding: 0.5rem 1rem;
font-size: var(--t-text-small);
font-weight: 500;
border-radius: var(--t-radius-button);
border: 1px solid var(--t-border-default);
background-color: var(--t-surface-base);
color: var(--t-text-secondary);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.preview-frame .filter-pill:hover {
background-color: var(--t-surface-sunken);
color: var(--t-text-primary);
}
.preview-frame .filter-pill-active {
background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
color: var(--t-text-inverse);
border-color: transparent;
}
.preview-frame .filter-pill-active:hover {
background-color: hsl(var(--t-accent-h) var(--t-accent-s) calc(var(--t-accent-l) - 5%));
color: var(--t-text-inverse);
}

View File

@ -18,10 +18,30 @@
--t-secondary-accent: #ea580c; --t-secondary-accent: #ea580c;
--t-sale-color: #dc2626; --t-sale-color: #dc2626;
/* Font size scale */ /* Font size scale - all sizes use em so they scale with --t-font-size-scale */
--t-font-size-scale: 1; --t-font-size-scale: 1;
--t-heading-weight: 600; --t-heading-weight: 600;
/*
* Type Scale - Limited to 6 body sizes for design consistency
* These use em units so they scale with the base font size setting
* Body: caption, small, base, large
* Display: xl, 2xl (for headings and hero text)
*/
--t-text-caption: 0.75em; /* ~12px at 16px base, ~14px at 18px base */
--t-text-small: 0.875em; /* ~14px at 16px base, ~16px at 18px base */
--t-text-base: 1em; /* matches base font size setting */
--t-text-large: 1.125em; /* ~18px at 16px base, ~20px at 18px base */
--t-text-xl: 1.25em; /* ~20px at 16px base, ~22px at 18px base */
--t-text-2xl: 1.5em; /* ~24px at 16px base, ~27px at 18px base */
/* Heading sizes - separate scale for headings */
--t-heading-sm: 1.25em; /* h4, h5, h6 */
--t-heading-md: 1.5em; /* h3 */
--t-heading-lg: 2em; /* h2 */
--t-heading-xl: 2.5em; /* h1 */
--t-heading-display: 3em; /* hero/display text */
/* Layout */ /* Layout */
--t-layout-max-width: 1400px; --t-layout-max-width: 1400px;
--t-button-style: filled; --t-button-style: filled;

View File

@ -43,11 +43,104 @@ const ColorSync = {
} }
} }
// Hook for PDP image lightbox
const Lightbox = {
mounted() {
const dialog = this.el
const lightboxImage = dialog.querySelector('#lightbox-image')
const lightboxCounter = dialog.querySelector('#lightbox-counter')
// Get images from data attribute
const getImages = () => {
try {
return JSON.parse(dialog.dataset.images || '[]')
} catch {
return []
}
}
const getCurrentIndex = () => parseInt(dialog.dataset.currentIndex || '0', 10)
const setCurrentIndex = (idx) => { dialog.dataset.currentIndex = idx.toString() }
const updateImage = () => {
const images = getImages()
const idx = getCurrentIndex()
if (images.length > 0 && lightboxImage) {
lightboxImage.src = images[idx]
if (lightboxCounter) {
lightboxCounter.textContent = `${idx + 1} / ${images.length}`
}
}
}
const nextImage = () => {
const images = getImages()
const newIdx = (getCurrentIndex() + 1) % images.length
setCurrentIndex(newIdx)
updateImage()
}
const prevImage = () => {
const images = getImages()
const newIdx = (getCurrentIndex() - 1 + images.length) % images.length
setCurrentIndex(newIdx)
updateImage()
}
const openLightbox = () => {
updateImage()
dialog.showModal()
}
const closeLightbox = () => {
dialog.close()
}
// Event listeners for custom events dispatched from LiveView.JS
dialog.addEventListener('pdp:open-lightbox', openLightbox)
dialog.addEventListener('pdp:close-lightbox', closeLightbox)
dialog.addEventListener('pdp:next-image', nextImage)
dialog.addEventListener('pdp:prev-image', prevImage)
// Close on clicking backdrop
dialog.addEventListener('click', (e) => {
if (e.target === dialog) {
closeLightbox()
}
})
// Keyboard navigation
dialog.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
nextImage()
} else if (e.key === 'ArrowLeft') {
prevImage()
} else if (e.key === 'Escape') {
closeLightbox()
}
})
// Store cleanup function
this.cleanup = () => {
dialog.removeEventListener('pdp:open-lightbox', openLightbox)
dialog.removeEventListener('pdp:close-lightbox', closeLightbox)
dialog.removeEventListener('pdp:next-image', nextImage)
dialog.removeEventListener('pdp:prev-image', prevImage)
}
},
destroyed() {
if (this.cleanup) {
this.cleanup()
}
}
}
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, { const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500, longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}, params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks, ColorSync}, hooks: {...colocatedHooks, ColorSync, Lightbox},
}) })
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
@ -55,6 +148,14 @@ topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
// Scroll preview frame to top when changing pages
window.addEventListener("phx:scroll-preview-top", (e) => {
const previewFrame = document.querySelector('.preview-frame')
if (previewFrame) {
previewFrame.scrollTop = 0
}
})
// connect if there are any LiveViews on the page // connect if there are any LiveViews on the page
liveSocket.connect() liveSocket.connect()

View File

@ -44,7 +44,6 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do
field :announcement_bar, :boolean, default: true field :announcement_bar, :boolean, default: true
field :sticky_header, :boolean, default: false field :sticky_header, :boolean, default: false
field :hover_image, :boolean, default: true field :hover_image, :boolean, default: true
field :quick_add, :boolean, default: true
field :show_prices, :boolean, default: true field :show_prices, :boolean, default: true
field :pdp_trust_badges, :boolean, default: true field :pdp_trust_badges, :boolean, default: true
field :pdp_reviews, :boolean, default: true field :pdp_reviews, :boolean, default: true
@ -85,7 +84,6 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do
:announcement_bar, :announcement_bar,
:sticky_header, :sticky_header,
:hover_image, :hover_image,
:quick_add,
:show_prices, :show_prices,
:pdp_trust_badges, :pdp_trust_badges,
:pdp_reviews, :pdp_reviews,
@ -105,5 +103,10 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do
|> validate_number(:header_position_y, greater_than_or_equal_to: 0, less_than_or_equal_to: 100) |> validate_number(:header_position_y, greater_than_or_equal_to: 0, less_than_or_equal_to: 100)
|> validate_inclusion(:layout_width, ~w(contained wide full)) |> validate_inclusion(:layout_width, ~w(contained wide full))
|> validate_inclusion(:card_shadow, ~w(none sm md lg)) |> validate_inclusion(:card_shadow, ~w(none sm md lg))
|> validate_inclusion(:font_size, ~w(small medium large))
|> validate_inclusion(:heading_weight, ~w(regular medium bold))
|> validate_inclusion(:button_style, ~w(filled outline soft))
|> validate_inclusion(:product_text_align, ~w(left center))
|> validate_inclusion(:image_aspect_ratio, ~w(square portrait landscape))
end end
end end

View File

@ -19,6 +19,12 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
.preview-frame, .shop-root { .preview-frame, .shop-root {
#{generate_accent(settings.accent_color)} #{generate_accent(settings.accent_color)}
#{generate_secondary_colors(settings)} #{generate_secondary_colors(settings)}
#{generate_font_size(settings.font_size)}
#{generate_heading_weight(settings.heading_weight)}
#{generate_layout_width(settings.layout_width)}
#{generate_button_style(settings.button_style)}
#{generate_product_text_align(settings.product_text_align)}
#{generate_image_aspect_ratio(settings.image_aspect_ratio)}
} }
""" """
|> String.trim() |> String.trim()
@ -225,6 +231,82 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
""" """
end end
# Font size variations
# Using 18px as base for better accessibility (WCAG recommends 18px+)
# Small: 18px, Medium: 19px, Large: 20px
defp generate_font_size("small") do
"--t-font-size-scale: 1.125;"
end
defp generate_font_size("medium") do
"--t-font-size-scale: 1.1875;"
end
defp generate_font_size("large") do
"--t-font-size-scale: 1.25;"
end
# Heading weight (override typography default)
defp generate_heading_weight("regular") do
"--t-heading-weight-override: 400;"
end
defp generate_heading_weight("medium") do
"--t-heading-weight-override: 500;"
end
defp generate_heading_weight("bold") do
"--t-heading-weight-override: 700;"
end
# Layout width
defp generate_layout_width("contained") do
"--t-layout-max-width: 1100px;"
end
defp generate_layout_width("wide") do
"--t-layout-max-width: 1400px;"
end
defp generate_layout_width("full") do
"--t-layout-max-width: 100%;"
end
# Button style
defp generate_button_style("filled") do
"--t-button-style: filled;"
end
defp generate_button_style("outline") do
"--t-button-style: outline;"
end
defp generate_button_style("soft") do
"--t-button-style: soft;"
end
# Product text alignment
defp generate_product_text_align("left") do
"--t-product-text-align: left;"
end
defp generate_product_text_align("center") do
"--t-product-text-align: center;"
end
# Image aspect ratio
defp generate_image_aspect_ratio("square") do
"--t-image-aspect-ratio: 1 / 1;"
end
defp generate_image_aspect_ratio("portrait") do
"--t-image-aspect-ratio: 3 / 4;"
end
defp generate_image_aspect_ratio("landscape") do
"--t-image-aspect-ratio: 4 / 3;"
end
# Convert hex color to HSL # Convert hex color to HSL
defp hex_to_hsl("#" <> hex), do: hex_to_hsl(hex) defp hex_to_hsl("#" <> hex), do: hex_to_hsl(hex)

View File

@ -122,7 +122,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
@impl true @impl true
def handle_event("change_preview_page", %{"page" => page_name}, socket) do def handle_event("change_preview_page", %{"page" => page_name}, socket) do
page_atom = String.to_existing_atom(page_name) page_atom = String.to_existing_atom(page_name)
{:noreply, assign(socket, :preview_page, page_atom)}
socket =
socket
|> assign(:preview_page, page_atom)
|> push_event("scroll-preview-top", %{})
{:noreply, socket}
end end
@impl true @impl true

View File

@ -310,7 +310,7 @@
</div> </div>
</div> </div>
<!-- Accent Color (stays in essentials) --> <!-- Accent Colors (stays in essentials) -->
<div class="mb-6"> <div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Accent colour</label> <label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Accent colour</label>
<form id="accent-color-form" phx-change="update_color" phx-value-field="accent_color" phx-hook="ColorSync"> <form id="accent-color-form" phx-change="update_color" phx-value-field="accent_color" phx-hook="ColorSync">
@ -327,6 +327,38 @@
</form> </form>
</div> </div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Hover colour</label>
<form id="secondary-accent-color-form" phx-change="update_color" phx-value-field="secondary_accent_color" phx-hook="ColorSync">
<div class="flex items-center gap-3">
<input
type="color"
id="secondary-accent-color-picker"
name="value"
value={@theme_settings.secondary_accent_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.secondary_accent_color %></span>
</div>
</form>
</div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Sale colour</label>
<form id="sale-color-form" phx-change="update_color" phx-value-field="sale_color" phx-hook="ColorSync">
<div class="flex items-center gap-3">
<input
type="color"
id="sale-color-picker"
name="value"
value={@theme_settings.sale_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.sale_color %></span>
</div>
</form>
</div>
<!-- Customise Section (collapsible accordion using native details/summary) --> <!-- Customise Section (collapsible accordion using native details/summary) -->
<details class="border-t border-base-300 mt-6 pt-4 group" id="customise-section" open={@customise_open}> <details class="border-t border-base-300 mt-6 pt-4 group" id="customise-section" open={@customise_open}>
<summary class="flex items-center justify-between w-full py-3 cursor-pointer list-none [&::-webkit-details-marker]:hidden" phx-click="toggle_customise"> <summary class="flex items-center justify-between w-full py-3 cursor-pointer list-none [&::-webkit-details-marker]:hidden" phx-click="toggle_customise">
@ -370,6 +402,52 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Font size</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"small", "Small"}, {"medium", "Medium"}, {"large", "Large"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="font_size"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.font_size == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Heading weight</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"regular", "Regular"}, {"medium", "Medium"}, {"bold", "Bold"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="heading_weight"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.heading_weight == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
</div> </div>
<!-- Colours Group --> <!-- Colours Group -->
@ -567,10 +645,33 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Button style</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"filled", "Filled"}, {"outline", "Outline"}, {"soft", "Soft"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="button_style"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.button_style == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
</div> </div>
<!-- Layout Group --> <!-- Products Group -->
<div class="mb-4"> <div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4"> <div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"></rect> <rect x="3" y="3" width="7" height="7"></rect>
@ -578,7 +679,7 @@
<rect x="14" y="14" width="7" height="7"></rect> <rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect> <rect x="3" y="14" width="7" height="7"></rect>
</svg> </svg>
<span class="text-sm font-semibold text-base-content">Layout</span> <span class="text-sm font-semibold text-base-content">Products</span>
</div> </div>
<div class="mb-4"> <div class="mb-4">
@ -603,6 +704,128 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Image aspect ratio</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"square", "Square"}, {"portrait", "Portrait"}, {"landscape", "Landscape"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="image_aspect_ratio"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.image_aspect_ratio == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Product text alignment</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"left", "Left"}, {"center", "Centre"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="product_text_align"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.product_text_align == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.hover_image}
phx-click="toggle_setting"
phx-value-field="hover_image"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Second image on hover</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.show_prices}
phx-click="toggle_setting"
phx-value-field="show_prices"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Show prices</span>
</label>
</div>
</div>
<!-- Product Page Group -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="3" y1="9" x2="21" y2="9"></line>
</svg>
<span class="text-sm font-semibold text-base-content">Product page</span>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_trust_badges}
phx-click="toggle_setting"
phx-value-field="pdp_trust_badges"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Trust badges</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_reviews}
phx-click="toggle_setting"
phx-value-field="pdp_reviews"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Reviews section</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_related_products}
phx-click="toggle_setting"
phx-value-field="pdp_related_products"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Related products</span>
</label>
</div>
</div> </div>
</div> </div>
</details> </details>
@ -674,7 +897,8 @@
data-header={@theme_settings.header_layout} data-header={@theme_settings.header_layout}
data-sticky={to_string(@theme_settings.sticky_header)} data-sticky={to_string(@theme_settings.sticky_header)}
data-layout={@theme_settings.layout_width} data-layout={@theme_settings.layout_width}
data-shadow={@theme_settings.card_shadow}> data-shadow={@theme_settings.card_shadow}
data-button-style={@theme_settings.button_style}>
<style> <style>
<%= Phoenix.HTML.raw(@generated_css) %> <%= Phoenix.HTML.raw(@generated_css) %>
</style> </style>

View File

@ -12,7 +12,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
~H""" ~H"""
<div <div
class="announcement-bar" class="announcement-bar"
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); text-align: center; padding: 0.5rem 1rem; font-size: 0.875rem;" style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); text-align: center; padding: 0.5rem 1rem; font-size: var(--t-text-small);"
> >
<p style="margin: 0;">Free delivery on orders over £40</p> <p style="margin: 0;">Free delivery on orders over £40</p>
</div> </div>
@ -39,7 +39,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
<div class="shop-logo" style="display: flex; align-items: center; position: relative; z-index: 1;"> <div class="shop-logo" style="display: flex; align-items: center; position: relative; z-index: 1;">
<%= case @theme_settings.logo_mode do %> <%= case @theme_settings.logo_mode do %>
<% "text-only" -> %> <% "text-only" -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);"> <span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %> <%= @theme_settings.site_name %>
</span> </span>
@ -51,7 +51,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"} style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"}
/> />
<% end %> <% end %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);"> <span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %> <%= @theme_settings.site_name %>
</span> </span>
@ -63,13 +63,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
style={"height: #{@theme_settings.logo_size}px; width: auto;"} style={"height: #{@theme_settings.logo_size}px; width: auto;"}
/> />
<% else %> <% else %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);"> <span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %> <%= @theme_settings.site_name %>
</span> </span>
<% end %> <% end %>
<% _ -> %> <% _ -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);"> <span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %> <%= @theme_settings.site_name %>
</span> </span>
<% end %> <% end %>
@ -107,7 +107,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
<line x1="3" y1="6" x2="21" y2="6"></line> <line x1="3" y1="6" x2="21" y2="6"></line>
<path d="M16 10a4 4 0 01-8 0"></path> <path d="M16 10a4 4 0 01-8 0"></path>
</svg> </svg>
<span class="cart-count" style="position: absolute; top: -4px; right: -4px; background: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); font-size: 11px; font-weight: 600; min-width: 18px; height: 18px; border-radius: 9999px; display: flex; align-items: center; justify-content: center; padding: 0 4px;">2</span> <span class="cart-count" style="position: absolute; top: -4px; right: -4px; background: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); font-size: var(--t-text-caption); font-weight: 600; min-width: 18px; height: 18px; border-radius: 9999px; display: flex; align-items: center; justify-content: center; padding: 0 4px;">2</span>
<span class="sr-only">Cart (2)</span> <span class="sr-only">Cart (2)</span>
</button> </button>
</div> </div>
@ -147,16 +147,16 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
<p class="mb-4 text-sm" style="color: var(--t-text-secondary);"> <p class="mb-4 text-sm" style="color: var(--t-text-secondary);">
Get 10% off your first order and be the first to know about new prints. Get 10% off your first order and be the first to know about new prints.
</p> </p>
<form class="flex gap-2"> <form class="flex flex-wrap gap-2">
<input <input
type="email" type="email"
placeholder="your@email.com" placeholder="your@email.com"
class="flex-1 px-4 py-2 text-sm" class="flex-1 min-w-0 px-4 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);" 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 <button
type="submit" type="submit"
class="px-6 py-2 font-medium transition-all text-sm" class="px-6 py-2 font-medium transition-all text-sm 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);" 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);"
> >
Subscribe Subscribe
@ -240,7 +240,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
style="position: fixed; top: 0; right: -400px; width: 400px; max-width: 90vw; height: 100vh; background: var(--t-surface-raised); z-index: 1001; display: flex; flex-direction: column; transition: right 0.3s ease; box-shadow: -4px 0 20px rgba(0,0,0,0.15);" style="position: fixed; top: 0; right: -400px; width: 400px; max-width: 90vw; height: 100vh; background: var(--t-surface-raised); z-index: 1001; display: flex; flex-direction: column; transition: right 0.3s ease; box-shadow: -4px 0 20px rgba(0,0,0,0.15);"
> >
<div class="cart-drawer-header" style="display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid var(--t-border-default);"> <div class="cart-drawer-header" style="display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid var(--t-border-default);">
<h2 style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--p-text-lg); color: var(--t-text-primary); margin: 0;">Your basket</h2> <h2 style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--t-text-large); color: var(--t-text-primary); margin: 0;">Your basket</h2>
<button <button
type="button" type="button"
class="cart-drawer-close" class="cart-drawer-close"
@ -260,13 +260,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
<div class="cart-drawer-item" style="display: flex; gap: 0.75rem; padding: 0.75rem 0; border-bottom: 1px solid var(--t-border-default);"> <div class="cart-drawer-item" style="display: flex; gap: 0.75rem; padding: 0.75rem 0; border-bottom: 1px solid var(--t-border-default);">
<div class="cart-drawer-item-image" style={"width: 60px; height: 60px; border-radius: var(--t-radius-card); background-size: cover; background-position: center; background-image: url('#{item.image}'); flex-shrink: 0;"}></div> <div class="cart-drawer-item-image" style={"width: 60px; height: 60px; border-radius: var(--t-radius-card); background-size: cover; background-position: center; background-image: url('#{item.image}'); flex-shrink: 0;"}></div>
<div class="cart-drawer-item-details" style="flex: 1;"> <div class="cart-drawer-item-details" style="flex: 1;">
<h3 style="font-family: var(--t-font-body); font-size: var(--p-text-sm); font-weight: 500; color: var(--t-text-primary); margin: 0 0 2px;"> <h3 style="font-family: var(--t-font-body); font-size: var(--t-text-small); font-weight: 500; color: var(--t-text-primary); margin: 0 0 2px;">
<%= item.name %> <%= item.name %>
</h3> </h3>
<p style="font-family: var(--t-font-body); font-size: var(--p-text-xs); color: var(--t-text-tertiary); margin: 0;"> <p style="font-family: var(--t-font-body); font-size: var(--t-text-caption); color: var(--t-text-tertiary); margin: 0;">
<%= item.variant %> <%= item.variant %>
</p> </p>
<p class="cart-drawer-item-price" style="color: var(--t-text-primary); font-weight: 500; margin-top: 4px; font-size: var(--p-text-sm);"> <p class="cart-drawer-item-price" style="color: var(--t-text-primary); font-weight: 500; margin-top: 4px; font-size: var(--t-text-small);">
<%= item.price %> <%= item.price %>
</p> </p>
</div> </div>
@ -275,7 +275,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
</div> </div>
<div class="cart-drawer-footer" style="padding: 1rem 1.5rem; border-top: 1px solid var(--t-border-default); background: var(--t-surface-sunken);"> <div class="cart-drawer-footer" style="padding: 1rem 1.5rem; border-top: 1px solid var(--t-border-default); background: var(--t-surface-sunken);">
<div class="cart-drawer-total" style="display: flex; justify-content: space-between; font-family: var(--t-font-body); font-size: var(--p-text-base); font-weight: 600; color: var(--t-text-primary); margin-bottom: 1rem;"> <div class="cart-drawer-total" style="display: flex; justify-content: space-between; font-family: var(--t-font-body); font-size: var(--t-text-base); font-weight: 600; color: var(--t-text-primary); margin-bottom: 1rem;">
<span>Subtotal</span> <span>Subtotal</span>
<span>£72.00</span> <span>£72.00</span>
</div> </div>
@ -288,10 +288,9 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
</button> </button>
<a <a
href="#" href="#"
phx-click="change_preview_page" phx-click={Phoenix.LiveView.JS.remove_class("open", to: "#cart-drawer") |> Phoenix.LiveView.JS.remove_class("open", to: "#cart-drawer-overlay") |> Phoenix.LiveView.JS.push("change_preview_page", value: %{page: "cart"})}
phx-value-page="cart"
class="cart-drawer-link" class="cart-drawer-link"
style="display: block; text-align: center; font-family: var(--t-font-body); font-size: var(--p-text-sm); color: var(--t-text-secondary); text-decoration: underline; cursor: pointer;" style="display: block; text-align: center; font-family: var(--t-font-body); font-size: var(--t-text-small); color: var(--t-text-secondary); text-decoration: underline; cursor: pointer;"
> >
View basket View basket
</a> </a>

View File

@ -41,14 +41,14 @@
Based in the Yorkshire countryside, I work from a small garden studio surrounded by the very nature that inspires each piece. Every print is checked by hand before being carefully packaged and sent on its way. Based in the Yorkshire countryside, I work from a small garden studio surrounded by the very nature that inspires each piece. Every print is checked by hand before being carefully packaged and sent on its way.
</p> </p>
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--p-text-xl); color: var(--t-text-primary);"> <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);">
Sustainability Sustainability
</h2> </h2>
<p class="mb-4" style="color: var(--t-text-secondary);"> <p class="mb-4" style="color: var(--t-text-secondary);">
I believe beautiful art shouldn't cost the earth. All prints are produced on FSC-certified paper, shipped in plastic-free packaging, and printed locally to reduce transport emissions. I believe beautiful art shouldn't cost the earth. All prints are produced on FSC-certified paper, shipped in plastic-free packaging, and printed locally to reduce transport emissions.
</p> </p>
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--p-text-xl); color: var(--t-text-primary);"> <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);">
The process The process
</h2> </h2>
<p class="mb-4" style="color: var(--t-text-secondary);"> <p class="mb-4" style="color: var(--t-text-secondary);">

View File

@ -20,77 +20,21 @@
</div> </div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <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"> <!-- Filter Bar: Category Pills + Sort -->
<!-- Filters Sidebar --> <div class="flex flex-wrap items-center justify-between gap-4 mb-6">
<div class="w-full md:w-64 flex-shrink-0"> <!-- Category Pills -->
<div class="p-4" style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"> <div class="filter-pills-container flex gap-2 overflow-x-auto">
<h3 class="font-semibold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary);"> <button class="filter-pill filter-pill-active">
Filter by Category All
</h3> </button>
<div class="space-y-2">
<%= for category <- @preview_data.categories do %> <%= for category <- @preview_data.categories do %>
<label class="flex items-center gap-2 cursor-pointer"> <button class="filter-pill">
<input <%= category.name %>
type="checkbox" </button>
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 %> <% end %>
</div> </div>
<div class="mt-6 pt-6" style="border-top: 1px solid var(--t-border-subtle);"> <!-- Sort Dropdown -->
<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 <select
class="px-4 py-2" 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);" 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);"
@ -103,8 +47,9 @@
</select> </select>
</div> </div>
<!-- Product Grid -->
<div class={[ <div class={[
"grid gap-6 grid-cols-1 sm:grid-cols-2", "product-grid grid grid-cols-1 sm:grid-cols-2",
case @theme_settings.grid_columns do case @theme_settings.grid_columns do
"2" -> "lg:grid-cols-2" "2" -> "lg:grid-cols-2"
"3" -> "lg:grid-cols-3" "3" -> "lg:grid-cols-3"
@ -119,15 +64,14 @@
class="product-card group overflow-hidden transition-all" class="product-card 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); cursor: pointer;" style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card); cursor: pointer;"
> >
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative"> <div class="product-image-container bg-gray-200 overflow-hidden relative">
<!-- Product Badge --> <!-- Product Badge -->
<%= if product.on_sale do %> <%= cond do %>
<% not product.in_stock -> %>
<span class="product-badge badge-sold-out">Sold out</span>
<% product.on_sale -> %>
<span class="product-badge badge-sale">Sale</span> <span class="product-badge badge-sale">Sale</span>
<% end %> <% true -> %>
<%= if not product.in_stock do %>
<div class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-20">
<span class="text-white font-semibold">Out of Stock</span>
</div>
<% end %> <% end %>
<!-- Primary Image --> <!-- Primary Image -->
<img <img
@ -136,7 +80,7 @@
class="product-image-primary w-full h-full object-cover transition-opacity duration-300" class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
/> />
<!-- Hover Image --> <!-- Hover Image -->
<%= if product[:hover_image_url] do %> <%= if @theme_settings.hover_image && product[:hover_image_url] do %>
<img <img
src={product.hover_image_url} src={product.hover_image_url}
alt={product.name} alt={product.name}
@ -144,14 +88,14 @@
/> />
<% end %> <% end %>
</div> </div>
<div class="p-4"> <div>
<p class="text-xs mb-1" style="color: var(--t-text-tertiary);"> <p class="text-xs mb-1" style="color: var(--t-text-tertiary);">
<%= product.category %> <%= product.category %>
</p> </p>
<h3 class="font-semibold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary);"> <h3 class="font-semibold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= product.name %> <%= product.name %>
</h3> </h3>
<div class="flex items-center justify-between"> <%= if @theme_settings.show_prices do %>
<div> <div>
<%= if product.on_sale do %> <%= 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));"> <span class="text-lg font-bold" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
@ -166,22 +110,12 @@
</span> </span>
<% end %> <% end %>
</div> </div>
<%= if product.in_stock do %>
<button
class="quick-add-btn 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);"
>
Quick add
</button>
<% end %> <% end %>
</div> </div>
</div> </div>
</div>
<% end %> <% end %>
</div> </div>
</div> </div>
</div>
</div>
<!-- Footer --> <!-- Footer -->
<SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_footer theme_settings={@theme_settings} /> <SimpleshopThemeWeb.ThemeLive.PreviewPages.shop_footer theme_settings={@theme_settings} />

View File

@ -13,7 +13,7 @@
</h1> </h1>
<p class="text-lg mb-12 text-center max-w-2xl mx-auto" style="color: var(--t-text-secondary);"> <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. Questions about your order or just want to say hello? Drop us a message and we'll get back to you as soon as we can.
</p> </p>
<div class="grid gap-8 md:grid-cols-2 mb-12"> <div class="grid gap-8 md:grid-cols-2 mb-12">
@ -87,44 +87,96 @@
<!-- Contact Info --> <!-- Contact Info -->
<div class="space-y-6"> <div class="space-y-6">
<!-- Order Tracking -->
<div <div
class="p-6" class="p-6"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);" 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> <h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Track your order</h3>
<p style="color: var(--t-text-secondary);">hello@example.com</p> <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> </div>
<!-- Helpful info -->
<div <div
class="p-6" class="p-6"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);" 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> <h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Handy to know</h3>
<p style="color: var(--t-text-secondary);">(555) 123-4567</p> <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> </div>
<!-- Get in touch -->
<div <div
class="p-6" class="p-6"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);" 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> <h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Get in touch</h3>
<p style="color: var(--t-text-secondary);"> <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;">
123 Main Street<br /> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
San Francisco, CA 94102<br /> <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" />
United States </svg>
hello@example.com
</a>
<p class="text-sm" style="color: var(--t-text-secondary);">
We typically respond within 24 hours
</p> </p>
</div> </div>
<div <!-- Social links -->
class="p-6" <div class="flex gap-4 justify-center">
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);" <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"
> >
<h3 class="font-bold mb-2" style="color: var(--t-text-primary);">Hours</h3> <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<p style="color: var(--t-text-secondary);"> <rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
Monday - Friday: 9am - 6pm<br /> <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
Saturday: 10am - 4pm<br /> <line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
Sunday: Closed </svg>
</p> </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> </div>
</div> </div>
</div> </div>

View File

@ -37,26 +37,35 @@
</button> </button>
</div> </div>
<div class="mt-12 grid gap-4 grid-cols-2 md:grid-cols-4 max-w-xl mx-auto"> <div class="product-grid 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 %> <%= for product <- Enum.take(@preview_data.products, 4) do %>
<div <div
class="product-card group overflow-hidden" class="product-card group overflow-hidden"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);" 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"> <div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
<img <img
src={product.image_url} src={product.image_url}
alt={product.name} alt={product.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
/> />
<%= if @theme_settings.hover_image && product[:hover_image_url] do %>
<img
src={product.hover_image_url}
alt={product.name}
class="product-image-hover w-full h-full object-cover"
/>
<% end %>
</div> </div>
<div class="p-2"> <div class="p-2">
<p class="text-xs font-semibold truncate" style="color: var(--t-text-primary);"> <p class="text-xs font-semibold truncate" style="color: var(--t-text-primary);">
<%= product.name %> <%= product.name %>
</p> </p>
<%= if @theme_settings.show_prices do %>
<p class="text-xs" style="color: var(--t-text-secondary);"> <p class="text-xs" style="color: var(--t-text-secondary);">
£<%= product.price / 100 %> £<%= product.price / 100 %>
</p> </p>
<% end %>
</div> </div>
</div> </div>
<% end %> <% end %>

View File

@ -49,7 +49,7 @@
</h2> </h2>
<div class={[ <div class={[
"grid gap-6 grid-cols-1 sm:grid-cols-2", "product-grid grid grid-cols-1 sm:grid-cols-2",
case @theme_settings.grid_columns do case @theme_settings.grid_columns do
"2" -> "lg:grid-cols-2" "2" -> "lg:grid-cols-2"
"3" -> "lg:grid-cols-3" "3" -> "lg:grid-cols-3"
@ -64,7 +64,7 @@
class="product-card group overflow-hidden transition-all hover:-translate-y-1" class="product-card group overflow-hidden transition-all hover:-translate-y-1"
style="background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;" style="background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;"
> >
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative"> <div class="product-image-container bg-gray-200 overflow-hidden relative">
<%= if product[:is_new] do %> <%= if product[:is_new] do %>
<span class="product-badge badge-new">New</span> <span class="product-badge badge-new">New</span>
<% end %> <% end %>
@ -76,30 +76,26 @@
alt={product.name} alt={product.name}
class="product-image-primary w-full h-full object-cover transition-opacity duration-300" class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
/> />
<%= if product[:hover_image_url] do %> <%= if @theme_settings.hover_image && product[:hover_image_url] do %>
<img <img
src={product.hover_image_url} src={product.hover_image_url}
alt={product.name} alt={product.name}
class="product-image-hover w-full h-full object-cover" class="product-image-hover w-full h-full object-cover"
/> />
<% end %> <% end %>
<button
class="absolute bottom-2 left-2 right-2 px-3 py-2 text-sm font-medium opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"
style="background-color: var(--t-surface-raised); color: var(--t-text-primary); border-radius: var(--t-radius-button);"
>
Quick add
</button>
</div> </div>
<div style="padding: var(--space-md);"> <div>
<h3 class="text-sm font-medium mb-1" style="color: var(--t-text-primary);"> <h3 class="text-sm font-medium mb-1" style="color: var(--t-text-primary);">
<%= product.name %> <%= product.name %>
</h3> </h3>
<%= if @theme_settings.show_prices do %>
<p class="text-sm" style="color: var(--t-text-secondary);"> <p class="text-sm" style="color: var(--t-text-secondary);">
<%= if product.on_sale do %> <%= if product.on_sale do %>
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span> <span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span>
<% end %> <% end %>
£<%= product.price / 100 %> £<%= product.price / 100 %>
</p> </p>
<% end %>
</div> </div>
</div> </div>
<% end %> <% end %>

View File

@ -12,35 +12,125 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8 flex items-center gap-2 text-sm" style="color: var(--t-text-secondary);"> <nav class="mb-8 flex items-center gap-2 text-sm" style="color: var(--t-text-secondary);">
<span>Home</span> <a href="#" phx-click="change_preview_page" phx-value-page="home" class="hover:underline">Home</a>
<span>/</span> <span>/</span>
<span><%= product.category %></span> <a href="#" phx-click="change_preview_page" phx-value-page="collection" class="hover:underline"><%= product.category %></a>
<span>/</span> <span>/</span>
<span style="color: var(--t-text-primary);"><%= product.name %></span> <span style="color: var(--t-text-primary);"><%= product.name %></span>
</div> </nav>
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16"> <div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16">
<!-- Product Images --> <!-- Product Images -->
<div> <div>
<div class="aspect-square bg-gray-200 mb-4 overflow-hidden" style="border-radius: var(--t-radius-image);"> <% gallery_images = [product.image_url, product.hover_image_url, product.image_url, product.hover_image_url] %>
<div
class="pdp-main-image-container aspect-square bg-gray-200 mb-4 overflow-hidden relative"
style="border-radius: var(--t-radius-image);"
phx-click={Phoenix.LiveView.JS.exec("data-show", to: "#pdp-lightbox")}
>
<img <img
id="pdp-main-image"
src={product.image_url} src={product.image_url}
alt={product.name} alt={product.name}
class="w-full h-full object-cover" class="w-full h-full object-cover"
/> />
<!-- Zoom icon overlay -->
<div class="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity bg-black/10">
<div class="w-12 h-12 bg-white/90 rounded-full flex items-center justify-center shadow-lg">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
</svg>
</div>
</div>
</div> </div>
<div class="grid grid-cols-4 gap-4"> <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 %> <%= for {img_url, idx} <- Enum.with_index(gallery_images) 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);"> <button
type="button"
class={"aspect-square bg-gray-200 cursor-pointer overflow-hidden pdp-thumbnail#{if idx == 0, do: " pdp-thumbnail-active", else: ""}"}
style="border-radius: var(--t-radius-image);"
data-index={idx}
phx-click={Phoenix.LiveView.JS.set_attribute({"src", img_url}, to: "#pdp-main-image")
|> Phoenix.LiveView.JS.set_attribute({"data-current-index", to_string(idx)}, to: "#pdp-lightbox")
|> Phoenix.LiveView.JS.remove_class("pdp-thumbnail-active", to: ".pdp-thumbnail")
|> Phoenix.LiveView.JS.add_class("pdp-thumbnail-active")}
>
<img <img
src={img_url} src={img_url}
alt={product.name} alt={product.name}
class="w-full h-full object-cover" class="w-full h-full object-cover"
/> />
</div> </button>
<% end %> <% end %>
</div> </div>
<style>
.pdp-thumbnail {
border: 2px solid var(--t-border-default);
transition: border-color 0.15s ease;
}
.pdp-thumbnail-active {
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
}
</style>
<!-- Image Lightbox -->
<dialog
class="lightbox"
id="pdp-lightbox"
aria-label="Product image gallery"
data-current-index="0"
data-images={Jason.encode!(gallery_images)}
data-show={Phoenix.LiveView.JS.exec("phx-show-lightbox", to: "#pdp-lightbox")}
phx-show-lightbox={Phoenix.LiveView.JS.dispatch("pdp:open-lightbox", to: "#pdp-lightbox")}
phx-hook="Lightbox"
>
<div class="lightbox-content">
<button
type="button"
class="lightbox-close"
aria-label="Close gallery"
phx-click={Phoenix.LiveView.JS.dispatch("pdp:close-lightbox", to: "#pdp-lightbox")}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<button
type="button"
class="lightbox-nav lightbox-prev"
aria-label="Previous image"
phx-click={Phoenix.LiveView.JS.dispatch("pdp:prev-image", to: "#pdp-lightbox")}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<figure class="lightbox-figure">
<div class="lightbox-image-container">
<img
class="lightbox-image"
id="lightbox-image"
src={product.image_url}
alt={product.name}
/>
</div>
<figcaption class="lightbox-caption"><%= product.name %></figcaption>
</figure>
<button
type="button"
class="lightbox-nav lightbox-next"
aria-label="Next image"
phx-click={Phoenix.LiveView.JS.dispatch("pdp:next-image", to: "#pdp-lightbox")}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
<div class="lightbox-counter" id="lightbox-counter">1 / <%= length(gallery_images) %></div>
</div>
</dialog>
</div> </div>
<!-- Product Info --> <!-- Product Info -->
@ -118,7 +208,8 @@
Add to basket Add to basket
</button> </button>
<!-- Features --> <!-- Features / Trust Badges -->
<%= if @theme_settings.pdp_trust_badges do %>
<div class="p-4 space-y-3" style="background-color: var(--t-surface-sunken); border-radius: var(--t-radius-card);"> <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"> <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));"> <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));">
@ -139,40 +230,129 @@
</div> </div>
</div> </div>
</div> </div>
<% end %>
</div> </div>
</div> </div>
<!-- Reviews Section -->
<%= if @theme_settings.pdp_reviews do %>
<div class="pdp-reviews py-12" style="border-top: 1px solid var(--t-border-default);">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<h2 class="text-2xl font-bold" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
Customer reviews
</h2>
<div class="flex items-center gap-3">
<div class="flex gap-0.5">
<%= for _i <- 1..5 do %>
<svg class="w-5 h-5" viewBox="0 0 20 20" style="color: #f59e0b;">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" fill="currentColor"/>
</svg>
<% end %>
</div>
<span class="text-sm" style="color: var(--t-text-secondary);">Based on 24 reviews</span>
</div>
</div>
<div class="space-y-6">
<!-- Review 1 -->
<article class="pb-6" style="border-bottom: 1px solid var(--t-border-subtle);">
<div class="flex items-center justify-between mb-2">
<div class="flex gap-0.5">
<%= for _i <- 1..5 do %>
<svg class="w-4 h-4" viewBox="0 0 20 20" style="color: #f59e0b;">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" fill="currentColor"/>
</svg>
<% end %>
</div>
<span class="text-xs" style="color: var(--t-text-tertiary);">2 weeks ago</span>
</div>
<h3 class="font-semibold mb-1" style="color: var(--t-text-primary);">Absolutely beautiful</h3>
<p class="text-sm mb-3" style="color: var(--t-text-secondary); line-height: 1.6;">
The quality exceeded my expectations. The colours are vibrant and the paper feels premium. It's now pride of place in my living room.
</p>
<div class="flex items-center gap-2">
<span class="text-sm font-medium" style="color: var(--t-text-primary);">Sarah M.</span>
<span class="text-xs px-2 py-0.5 rounded" style="background-color: var(--t-surface-sunken); color: var(--t-text-tertiary);">Verified purchase</span>
</div>
</article>
<!-- Review 2 -->
<article class="pb-6" style="border-bottom: 1px solid var(--t-border-subtle);">
<div class="flex items-center justify-between mb-2">
<div class="flex gap-0.5">
<%= for i <- 1..5 do %>
<svg class="w-4 h-4" viewBox="0 0 20 20" style={"color: #{if i <= 4, do: "#f59e0b", else: "var(--t-border-default)"};"}>
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" fill="currentColor"/>
</svg>
<% end %>
</div>
<span class="text-xs" style="color: var(--t-text-tertiary);">1 month ago</span>
</div>
<h3 class="font-semibold mb-1" style="color: var(--t-text-primary);">Great gift</h3>
<p class="text-sm mb-3" style="color: var(--t-text-secondary); line-height: 1.6;">
Bought this as a gift and it arrived beautifully packaged. Fast shipping too. Would definitely order again.
</p>
<div class="flex items-center gap-2">
<span class="text-sm font-medium" style="color: var(--t-text-primary);">James T.</span>
<span class="text-xs px-2 py-0.5 rounded" style="background-color: var(--t-surface-sunken); color: var(--t-text-tertiary);">Verified purchase</span>
</div>
</article>
</div>
<button
class="mt-6 px-6 py-2 text-sm font-medium transition-all mx-auto block"
style="background-color: transparent; color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-button);"
>
Load more reviews
</button>
</div>
<% end %>
<!-- Related Products --> <!-- Related Products -->
<div> <%= 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);"> <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 You might also like
</h2> </h2>
<div class="grid gap-6 grid-cols-2 md:grid-cols-4"> <div class="product-grid grid gap-6 grid-cols-2 md:grid-cols-4">
<%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %> <%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %>
<div <a
class="product-card group overflow-hidden" href="#"
phx-click="change_preview_page"
phx-value-page="pdp"
class="product-card group overflow-hidden cursor-pointer"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);" 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"> <div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
<img <img
src={related_product.image_url} src={related_product.image_url}
alt={related_product.name} alt={related_product.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
/> />
<%= if @theme_settings.hover_image && related_product[:hover_image_url] do %>
<img
src={related_product.hover_image_url}
alt={related_product.name}
class="product-image-hover w-full h-full object-cover"
/>
<% end %>
</div> </div>
<div class="p-3"> <div class="p-3">
<h3 class="font-semibold text-sm mb-1" style="font-family: var(--t-font-heading); color: var(--t-text-primary);"> <h3 class="font-semibold text-sm mb-1" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= related_product.name %> <%= related_product.name %>
</h3> </h3>
<%= if @theme_settings.show_prices do %>
<p class="font-bold" style="color: var(--t-text-primary);"> <p class="font-bold" style="color: var(--t-text-primary);">
$<%= related_product.price / 100 %> £<%= related_product.price / 100 %>
</p> </p>
<% end %>
</div> </div>
</div> </a>
<% end %> <% end %>
</div> </div>
</div> </div>
<% end %>
</div> </div>
<!-- Footer --> <!-- Footer -->