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:
parent
98a9e3b3d4
commit
37653e5e7a
@ -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);
|
||||
}
|
||||
|
||||
/* =============================================
|
||||
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-badge {
|
||||
position: absolute;
|
||||
@ -276,6 +359,11 @@
|
||||
color: var(--t-text-inverse);
|
||||
}
|
||||
|
||||
.badge-sold-out {
|
||||
background-color: var(--t-text-tertiary, #737373);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Product Hover Image */
|
||||
.product-image-container {
|
||||
position: relative;
|
||||
@ -292,20 +380,46 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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-link:hover {
|
||||
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-btn:hover {
|
||||
background-color: var(--t-surface-sunken);
|
||||
color: var(--t-text-primary);
|
||||
color: var(--t-secondary-accent, var(--t-text-primary));
|
||||
}
|
||||
|
||||
/* Search Modal Animation */
|
||||
@ -318,3 +432,225 @@
|
||||
transform: translateY(0);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -18,10 +18,30 @@
|
||||
--t-secondary-accent: #ea580c;
|
||||
--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-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 */
|
||||
--t-layout-max-width: 1400px;
|
||||
--t-button-style: filled;
|
||||
|
||||
103
assets/js/app.js
103
assets/js/app.js
@ -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 liveSocket = new LiveSocket("/live", Socket, {
|
||||
longPollFallbackMs: 2500,
|
||||
params: {_csrf_token: csrfToken},
|
||||
hooks: {...colocatedHooks, ColorSync},
|
||||
hooks: {...colocatedHooks, ColorSync, Lightbox},
|
||||
})
|
||||
|
||||
// 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-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
|
||||
liveSocket.connect()
|
||||
|
||||
|
||||
@ -44,7 +44,6 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do
|
||||
field :announcement_bar, :boolean, default: true
|
||||
field :sticky_header, :boolean, default: false
|
||||
field :hover_image, :boolean, default: true
|
||||
field :quick_add, :boolean, default: true
|
||||
field :show_prices, :boolean, default: true
|
||||
field :pdp_trust_badges, :boolean, default: true
|
||||
field :pdp_reviews, :boolean, default: true
|
||||
@ -85,7 +84,6 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do
|
||||
:announcement_bar,
|
||||
:sticky_header,
|
||||
:hover_image,
|
||||
:quick_add,
|
||||
:show_prices,
|
||||
:pdp_trust_badges,
|
||||
: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_inclusion(:layout_width, ~w(contained wide full))
|
||||
|> 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
|
||||
|
||||
@ -19,6 +19,12 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
|
||||
.preview-frame, .shop-root {
|
||||
#{generate_accent(settings.accent_color)}
|
||||
#{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()
|
||||
@ -225,6 +231,82 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
|
||||
"""
|
||||
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
|
||||
defp hex_to_hsl("#" <> hex), do: hex_to_hsl(hex)
|
||||
|
||||
|
||||
@ -122,7 +122,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do
|
||||
@impl true
|
||||
def handle_event("change_preview_page", %{"page" => page_name}, socket) do
|
||||
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
|
||||
|
||||
@impl true
|
||||
|
||||
@ -310,7 +310,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Accent Color (stays in essentials) -->
|
||||
<!-- Accent Colors (stays in essentials) -->
|
||||
<div class="mb-6">
|
||||
<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">
|
||||
@ -327,6 +327,38 @@
|
||||
</form>
|
||||
</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) -->
|
||||
<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">
|
||||
@ -370,6 +402,52 @@
|
||||
<% end %>
|
||||
</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>
|
||||
|
||||
<!-- Colours Group -->
|
||||
@ -567,10 +645,33 @@
|
||||
<% end %>
|
||||
</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>
|
||||
|
||||
<!-- Layout Group -->
|
||||
<div class="mb-4">
|
||||
<!-- Products Group -->
|
||||
<div class="mb-6 pb-6 border-b border-base-200">
|
||||
<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="7" height="7"></rect>
|
||||
@ -578,7 +679,7 @@
|
||||
<rect x="14" y="14" width="7" height="7"></rect>
|
||||
<rect x="3" y="14" width="7" height="7"></rect>
|
||||
</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 class="mb-4">
|
||||
@ -603,6 +704,128 @@
|
||||
<% end %>
|
||||
</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>
|
||||
</details>
|
||||
@ -674,7 +897,8 @@
|
||||
data-header={@theme_settings.header_layout}
|
||||
data-sticky={to_string(@theme_settings.sticky_header)}
|
||||
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>
|
||||
<%= Phoenix.HTML.raw(@generated_css) %>
|
||||
</style>
|
||||
|
||||
@ -12,7 +12,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
|
||||
~H"""
|
||||
<div
|
||||
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>
|
||||
</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;">
|
||||
<%= case @theme_settings.logo_mode do %>
|
||||
<% "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 %>
|
||||
</span>
|
||||
|
||||
@ -51,7 +51,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
|
||||
style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"}
|
||||
/>
|
||||
<% 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 %>
|
||||
</span>
|
||||
|
||||
@ -63,13 +63,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
|
||||
style={"height: #{@theme_settings.logo_size}px; width: auto;"}
|
||||
/>
|
||||
<% 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 %>
|
||||
</span>
|
||||
<% 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 %>
|
||||
</span>
|
||||
<% end %>
|
||||
@ -107,7 +107,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<path d="M16 10a4 4 0 01-8 0"></path>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
@ -147,16 +147,16 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
|
||||
<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.
|
||||
</p>
|
||||
<form class="flex gap-2">
|
||||
<form class="flex flex-wrap gap-2">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
class="flex-1 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);"
|
||||
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); min-width: 150px;"
|
||||
/>
|
||||
<button
|
||||
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);"
|
||||
>
|
||||
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);"
|
||||
>
|
||||
<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
|
||||
type="button"
|
||||
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-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;">
|
||||
<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 %>
|
||||
</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 %>
|
||||
</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 %>
|
||||
</p>
|
||||
</div>
|
||||
@ -275,7 +275,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
|
||||
</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-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>£72.00</span>
|
||||
</div>
|
||||
@ -288,10 +288,9 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
|
||||
</button>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="cart"
|
||||
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"})}
|
||||
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
|
||||
</a>
|
||||
|
||||
@ -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.
|
||||
</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
|
||||
</h2>
|
||||
<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.
|
||||
</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
|
||||
</h2>
|
||||
<p class="mb-4" style="color: var(--t-text-secondary);">
|
||||
|
||||
@ -20,91 +20,36 @@
|
||||
</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>
|
||||
<!-- Filter Bar: Category Pills + Sort -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<!-- Category Pills -->
|
||||
<div class="filter-pills-container flex gap-2 overflow-x-auto">
|
||||
<button class="filter-pill filter-pill-active">
|
||||
All
|
||||
</button>
|
||||
<%= for category <- @preview_data.categories do %>
|
||||
<button class="filter-pill">
|
||||
<%= category.name %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!-- Sort Dropdown -->
|
||||
<select
|
||||
class="px-4 py-2"
|
||||
style="background-color: var(--t-surface-raised); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input);"
|
||||
>
|
||||
<option>Sort by: Featured</option>
|
||||
<option>Price: Low to High</option>
|
||||
<option>Price: High to Low</option>
|
||||
<option>Newest</option>
|
||||
<option>Best Selling</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class={[
|
||||
"grid gap-6 grid-cols-1 sm:grid-cols-2",
|
||||
<!-- Product Grid -->
|
||||
<div class={[
|
||||
"product-grid grid grid-cols-1 sm:grid-cols-2",
|
||||
case @theme_settings.grid_columns do
|
||||
"2" -> "lg:grid-cols-2"
|
||||
"3" -> "lg:grid-cols-3"
|
||||
@ -119,15 +64,14 @@
|
||||
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;"
|
||||
>
|
||||
<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 -->
|
||||
<%= if product.on_sale do %>
|
||||
<span class="product-badge badge-sale">Sale</span>
|
||||
<% end %>
|
||||
<%= 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>
|
||||
<%= 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>
|
||||
<% true -> %>
|
||||
<% end %>
|
||||
<!-- Primary Image -->
|
||||
<img
|
||||
@ -136,7 +80,7 @@
|
||||
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
|
||||
/>
|
||||
<!-- Hover Image -->
|
||||
<%= if product[:hover_image_url] do %>
|
||||
<%= if @theme_settings.hover_image && product[:hover_image_url] do %>
|
||||
<img
|
||||
src={product.hover_image_url}
|
||||
alt={product.name}
|
||||
@ -144,14 +88,14 @@
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<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">
|
||||
<%= if @theme_settings.show_prices do %>
|
||||
<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));">
|
||||
@ -166,21 +110,11 @@
|
||||
</span>
|
||||
<% end %>
|
||||
</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 %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</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.
|
||||
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>
|
||||
|
||||
<div class="grid gap-8 md:grid-cols-2 mb-12">
|
||||
@ -87,44 +87,96 @@
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="space-y-6">
|
||||
<!-- Order Tracking -->
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-2" style="color: var(--t-text-primary);">Email</h3>
|
||||
<p style="color: var(--t-text-secondary);">hello@example.com</p>
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Track your order</h3>
|
||||
<p class="text-sm mb-3" style="color: var(--t-text-secondary);">
|
||||
Enter your email and we'll send you a link to check your order status.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
class="flex-1 min-w-0 px-3 py-2 text-sm"
|
||||
style="background-color: var(--t-surface-base); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input); min-width: 150px;"
|
||||
/>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium whitespace-nowrap"
|
||||
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); border-radius: var(--t-radius-button);"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Helpful info -->
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-2" style="color: var(--t-text-primary);">Phone</h3>
|
||||
<p style="color: var(--t-text-secondary);">(555) 123-4567</p>
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Handy to know</h3>
|
||||
<ul class="space-y-2 text-sm" style="color: var(--t-text-secondary);">
|
||||
<li class="flex items-start gap-2">
|
||||
<span style="color: var(--t-text-tertiary);">•</span>
|
||||
<span><strong style="color: var(--t-text-primary);">Printing:</strong> 2-5 business days</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span style="color: var(--t-text-tertiary);">•</span>
|
||||
<span><strong style="color: var(--t-text-primary);">Delivery:</strong> 3-7 business days after printing</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span style="color: var(--t-text-tertiary);">•</span>
|
||||
<span><strong style="color: var(--t-text-primary);">Returns:</strong> Happy to help with faulty or damaged items</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Get in touch -->
|
||||
<div
|
||||
class="p-6"
|
||||
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
|
||||
>
|
||||
<h3 class="font-bold mb-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
|
||||
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Get in touch</h3>
|
||||
<a href="mailto:hello@example.com" class="flex items-center gap-2 mb-2" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); text-decoration: none;">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
hello@example.com
|
||||
</a>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||
We typically respond within 24 hours
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- Social links -->
|
||||
<div class="flex gap-4 justify-center">
|
||||
<a
|
||||
href="#"
|
||||
class="social-link w-9 h-9 flex items-center justify-center transition-all"
|
||||
style="color: var(--t-text-secondary); border-radius: var(--t-radius-button);"
|
||||
aria-label="Instagram"
|
||||
>
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
|
||||
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
|
||||
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="social-link w-9 h-9 flex items-center justify-center transition-all"
|
||||
style="color: var(--t-text-secondary); border-radius: var(--t-radius-button);"
|
||||
aria-label="Pinterest"
|
||||
>
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M8 12c0-2.2 1.8-4 4-4s4 1.8 4 4-1.8 4-4 4"></path>
|
||||
<line x1="12" y1="16" x2="9" y2="21"></line>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -37,26 +37,35 @@
|
||||
</button>
|
||||
</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 %>
|
||||
<div
|
||||
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);"
|
||||
>
|
||||
<div class="aspect-square bg-gray-200 overflow-hidden">
|
||||
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
|
||||
<img
|
||||
src={product.image_url}
|
||||
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 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>
|
||||
<%= if @theme_settings.show_prices do %>
|
||||
<p class="text-xs" style="color: var(--t-text-secondary);">
|
||||
£<%= product.price / 100 %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
</h2>
|
||||
|
||||
<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
|
||||
"2" -> "lg:grid-cols-2"
|
||||
"3" -> "lg:grid-cols-3"
|
||||
@ -64,7 +64,7 @@
|
||||
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;"
|
||||
>
|
||||
<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 %>
|
||||
<span class="product-badge badge-new">New</span>
|
||||
<% end %>
|
||||
@ -76,30 +76,26 @@
|
||||
alt={product.name}
|
||||
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
|
||||
src={product.hover_image_url}
|
||||
alt={product.name}
|
||||
class="product-image-hover w-full h-full object-cover"
|
||||
/>
|
||||
<% 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 style="padding: var(--space-md);">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-1" style="color: var(--t-text-primary);">
|
||||
<%= product.name %>
|
||||
</h3>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||
<%= if product.on_sale do %>
|
||||
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span>
|
||||
<% end %>
|
||||
£<%= product.price / 100 %>
|
||||
</p>
|
||||
<%= if @theme_settings.show_prices do %>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||
<%= if product.on_sale do %>
|
||||
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span>
|
||||
<% end %>
|
||||
£<%= product.price / 100 %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@ -12,35 +12,125 @@
|
||||
|
||||
<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>
|
||||
<nav class="mb-8 flex items-center gap-2 text-sm" style="color: var(--t-text-secondary);">
|
||||
<a href="#" phx-click="change_preview_page" phx-value-page="home" class="hover:underline">Home</a>
|
||||
<span>/</span>
|
||||
<span><%= product.category %></span>
|
||||
<a href="#" phx-click="change_preview_page" phx-value-page="collection" class="hover:underline"><%= product.category %></a>
|
||||
<span>/</span>
|
||||
<span style="color: var(--t-text-primary);"><%= product.name %></span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<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);">
|
||||
<% 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
|
||||
id="pdp-main-image"
|
||||
src={product.image_url}
|
||||
alt={product.name}
|
||||
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 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);">
|
||||
<%= for {img_url, idx} <- Enum.with_index(gallery_images) do %>
|
||||
<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
|
||||
src={img_url}
|
||||
alt={product.name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<% end %>
|
||||
</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>
|
||||
|
||||
<!-- Product Info -->
|
||||
@ -118,61 +208,151 @@
|
||||
Add to basket
|
||||
</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 Delivery</p>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">On orders over £40</p>
|
||||
<!-- 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="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 Delivery</p>
|
||||
<p class="text-sm" style="color: var(--t-text-secondary);">On orders over £40</p>
|
||||
</div>
|
||||
</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 might 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="product-card 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 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>
|
||||
<% end %>
|
||||
</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 -->
|
||||
<%= if @theme_settings.pdp_related_products do %>
|
||||
<div class="py-12" style="border-top: 1px solid var(--t-border-default);">
|
||||
<h2 class="text-2xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
|
||||
You might also like
|
||||
</h2>
|
||||
|
||||
<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 %>
|
||||
<a
|
||||
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);"
|
||||
>
|
||||
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
|
||||
<img
|
||||
src={related_product.image_url}
|
||||
alt={related_product.name}
|
||||
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 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>
|
||||
<%= if @theme_settings.show_prices do %>
|
||||
<p class="font-bold" style="color: var(--t-text-primary);">
|
||||
£<%= related_product.price / 100 %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
Loading…
Reference in New Issue
Block a user