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);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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 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()
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);">
|
||||||
|
|||||||
@ -20,91 +20,36 @@
|
|||||||
</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 %>
|
<button class="filter-pill">
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
<%= category.name %>
|
||||||
<input
|
</button>
|
||||||
type="checkbox"
|
<% end %>
|
||||||
class="rounded"
|
|
||||||
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); border-radius: var(--t-radius-input);"
|
|
||||||
/>
|
|
||||||
<span style="color: var(--t-text-primary);"><%= category.name %></span>
|
|
||||||
<span class="ml-auto text-sm" style="color: var(--t-text-tertiary);">
|
|
||||||
<%= category.product_count %>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6 pt-6" style="border-top: 1px solid var(--t-border-subtle);">
|
|
||||||
<h3 class="font-semibold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
|
|
||||||
Price Range
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="rounded"
|
|
||||||
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
|
|
||||||
/>
|
|
||||||
<span style="color: var(--t-text-primary);">Under $25</span>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="rounded"
|
|
||||||
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
|
|
||||||
/>
|
|
||||||
<span style="color: var(--t-text-primary);">$25 - $50</span>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="rounded"
|
|
||||||
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
|
|
||||||
/>
|
|
||||||
<span style="color: var(--t-text-primary);">$50 - $100</span>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="rounded"
|
|
||||||
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
|
|
||||||
/>
|
|
||||||
<span style="color: var(--t-text-primary);">Over $100</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Product Grid -->
|
<!-- Sort Dropdown -->
|
||||||
<div class="flex-1">
|
<select
|
||||||
<div class="flex items-center justify-between mb-6">
|
class="px-4 py-2"
|
||||||
<p style="color: var(--t-text-secondary);">
|
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);"
|
||||||
Showing all <%= length(@preview_data.products) %> products
|
>
|
||||||
</p>
|
<option>Sort by: Featured</option>
|
||||||
<select
|
<option>Price: Low to High</option>
|
||||||
class="px-4 py-2"
|
<option>Price: High to Low</option>
|
||||||
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>Newest</option>
|
||||||
>
|
<option>Best Selling</option>
|
||||||
<option>Sort by: Featured</option>
|
</select>
|
||||||
<option>Price: Low to High</option>
|
</div>
|
||||||
<option>Price: High to Low</option>
|
|
||||||
<option>Newest</option>
|
|
||||||
<option>Best Selling</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={[
|
<!-- Product Grid -->
|
||||||
"grid gap-6 grid-cols-1 sm:grid-cols-2",
|
<div class={[
|
||||||
|
"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 %>
|
||||||
<span class="product-badge badge-sale">Sale</span>
|
<% not product.in_stock -> %>
|
||||||
<% end %>
|
<span class="product-badge badge-sold-out">Sold out</span>
|
||||||
<%= if not product.in_stock do %>
|
<% product.on_sale -> %>
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-20">
|
<span class="product-badge badge-sale">Sale</span>
|
||||||
<span class="text-white font-semibold">Out of Stock</span>
|
<% true -> %>
|
||||||
</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,21 +110,11 @@
|
|||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= if product.in_stock do %>
|
<% end %>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
@ -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="#"
|
||||||
<h3 class="font-bold mb-2" style="color: var(--t-text-primary);">Hours</h3>
|
class="social-link w-9 h-9 flex items-center justify-center transition-all"
|
||||||
<p style="color: var(--t-text-secondary);">
|
style="color: var(--t-text-secondary); border-radius: var(--t-radius-button);"
|
||||||
Monday - Friday: 9am - 6pm<br />
|
aria-label="Instagram"
|
||||||
Saturday: 10am - 4pm<br />
|
>
|
||||||
Sunday: Closed
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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>
|
||||||
<p class="text-xs" style="color: var(--t-text-secondary);">
|
<%= if @theme_settings.show_prices do %>
|
||||||
£<%= product.price / 100 %>
|
<p class="text-xs" style="color: var(--t-text-secondary);">
|
||||||
</p>
|
£<%= product.price / 100 %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@ -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>
|
||||||
<p class="text-sm" style="color: var(--t-text-secondary);">
|
<%= if @theme_settings.show_prices do %>
|
||||||
<%= if product.on_sale do %>
|
<p class="text-sm" style="color: var(--t-text-secondary);">
|
||||||
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span>
|
<%= if product.on_sale do %>
|
||||||
<% end %>
|
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span>
|
||||||
£<%= product.price / 100 %>
|
<% end %>
|
||||||
</p>
|
£<%= product.price / 100 %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@ -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,61 +208,151 @@
|
|||||||
Add to basket
|
Add to basket
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Features -->
|
<!-- Features / Trust Badges -->
|
||||||
<div class="p-4 space-y-3" style="background-color: var(--t-surface-sunken); border-radius: var(--t-radius-card);">
|
<%= if @theme_settings.pdp_trust_badges do %>
|
||||||
<div class="flex items-start gap-3">
|
<div class="p-4 space-y-3" style="background-color: var(--t-surface-sunken); border-radius: var(--t-radius-card);">
|
||||||
<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));">
|
<div class="flex items-start gap-3">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
<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>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
<div>
|
</svg>
|
||||||
<p class="font-semibold" style="color: var(--t-text-primary);">Free Delivery</p>
|
<div>
|
||||||
<p class="text-sm" style="color: var(--t-text-secondary);">On orders over £40</p>
|
<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>
|
<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));">
|
<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" />
|
||||||
<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>
|
||||||
</svg>
|
<div>
|
||||||
<div>
|
<p class="font-semibold" style="color: var(--t-text-primary);">Easy Returns</p>
|
||||||
<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>
|
||||||
<p class="text-sm" style="color: var(--t-text-secondary);">30-day return policy</p>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% 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 -->
|
||||||
|
<%= 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>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user