feat: product page layout toggle (flat/card), fix email shortcode rendering
- Add layout_style setting (flat default) to product appearance
- AppearanceController: sanitize & persist layout_style, add to default settings
- Admin SPA: Layout Style select in Appearance > Product
- Customer SPA: useEffect targets <main> bg-white in flat mode (full-width),
card mode uses per-section white floating cards on gray background
- Accordion sections styled per mode: flat=border-t dividers, card=white cards
- Fix email shortcode gaps (EmailRenderer, EmailManager)
- Add missing variables: return_url, contact_url, account_url (alias),
payment_error_reason, order_items_list (alias for order_items_table)
- Fix customer_note extra_data key mismatch (note → customer_note)
- Pass low_stock_threshold via extra_data in low_stock email send
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||
|
||||
interface FeatureItem {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
// Post-card fields (from related_posts dynamic source)
|
||||
url?: string;
|
||||
featured_image?: string;
|
||||
excerpt?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
interface FeatureGridSectionProps {
|
||||
@@ -26,15 +32,26 @@ export function FeatureGridSection({
|
||||
elementStyles,
|
||||
styles,
|
||||
}: FeatureGridSectionProps & { features?: FeatureItem[], styles?: Record<string, any> }) {
|
||||
// Use items or features (priority to items if both exist, but usually only one comes from props)
|
||||
const heightMap: Record<string, string> = {
|
||||
'default': 'py-12 md:py-20',
|
||||
'small': 'py-8 md:py-12',
|
||||
'medium': 'py-16 md:py-24',
|
||||
'large': 'py-24 md:py-36',
|
||||
'fullscreen': 'min-h-screen flex flex-col justify-center',
|
||||
};
|
||||
const customPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||
const heightClasses = customPadding ? '' : (heightMap[styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||
const listItems = items.length > 0 ? items : features;
|
||||
|
||||
const gridCols = {
|
||||
'grid-2': 'md:grid-cols-2',
|
||||
'grid-3': 'md:grid-cols-3',
|
||||
'grid-4': 'md:grid-cols-2 lg:grid-cols-4',
|
||||
}[layout] || 'md:grid-cols-3';
|
||||
|
||||
// Helper to get text styles (including font family)
|
||||
// Detect if these are post-cards (from related_posts) — they have a url field
|
||||
const isPostCards = listItems.some(item => !!item.url);
|
||||
|
||||
const getTextStyles = (elementName: string) => {
|
||||
const styles = elementStyles?.[elementName] || {};
|
||||
return {
|
||||
@@ -60,6 +77,21 @@ export function FeatureGridSection({
|
||||
const headingStyle = getTextStyles('heading');
|
||||
const featureItemStyle = getTextStyles('feature_item');
|
||||
|
||||
const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
||||
const sectionBg = getSectionBackground(styles);
|
||||
|
||||
// Helper to get background style for dynamic schemes
|
||||
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
||||
if (hasCustomBackground) return sectionBg.style;
|
||||
if (colorScheme === 'primary') {
|
||||
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
||||
}
|
||||
if (colorScheme === 'secondary') {
|
||||
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
@@ -67,23 +99,25 @@ export function FeatureGridSection({
|
||||
'wn-section wn-feature-grid',
|
||||
`wn-feature-grid--${layout}`,
|
||||
`wn-scheme--${colorScheme}`,
|
||||
'py-12 md:py-24',
|
||||
heightClasses,
|
||||
{
|
||||
// 'bg-white': colorScheme === 'default', // Removed for global styling
|
||||
'bg-muted': colorScheme === 'muted',
|
||||
'bg-primary text-primary-foreground': colorScheme === 'primary',
|
||||
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
||||
'text-primary-foreground': colorScheme === 'primary' && !hasCustomBackground,
|
||||
}
|
||||
)}
|
||||
style={getBackgroundStyle()}
|
||||
>
|
||||
<div className={cn(
|
||||
"mx-auto px-4",
|
||||
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
||||
styles?.contentWidth === 'full' ? 'w-full'
|
||||
: styles?.contentWidth === 'boxed' ? 'max-w-5xl'
|
||||
: 'container'
|
||||
)}>
|
||||
{heading && (
|
||||
<h2
|
||||
className={cn(
|
||||
"wn-features__heading text-center mb-12",
|
||||
!elementStyles?.heading?.fontSize && "text-3xl md:text-4xl",
|
||||
"wn-features__heading text-center mb-10",
|
||||
!elementStyles?.heading?.fontSize && "text-2xl md:text-3xl lg:text-4xl",
|
||||
!elementStyles?.heading?.fontWeight && "font-bold",
|
||||
headingStyle.classNames
|
||||
)}
|
||||
@@ -93,59 +127,122 @@ export function FeatureGridSection({
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<div className={cn('grid gap-8', gridCols)}>
|
||||
{listItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'wn-feature-grid__item',
|
||||
'p-6 rounded-xl',
|
||||
!featureItemStyle.style?.backgroundColor && {
|
||||
'bg-white shadow-lg': colorScheme !== 'primary',
|
||||
'bg-white/10': colorScheme === 'primary',
|
||||
},
|
||||
featureItemStyle.classNames
|
||||
)}
|
||||
style={featureItemStyle.style}
|
||||
>
|
||||
{item.icon && (() => {
|
||||
const IconComponent = (LucideIcons as any)[item.icon];
|
||||
if (!IconComponent) return null;
|
||||
return (
|
||||
<div className="wn-feature-grid__icon mb-4 inline-block p-3 rounded-full bg-primary/10 text-primary">
|
||||
<IconComponent className="w-8 h-8" />
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{item.title && (
|
||||
<h3
|
||||
<div className={cn('grid gap-6', gridCols)}>
|
||||
{listItems.map((item, index) => {
|
||||
// ── Post Card (from related_posts) ──────────────────────────
|
||||
if (isPostCards) {
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={item.url || '#'}
|
||||
className={cn(
|
||||
"wn-feature-grid__item-title mb-3",
|
||||
!featureItemStyle.classNames && "text-xl font-semibold"
|
||||
'wn-post-card group block rounded-xl overflow-hidden transition-all duration-200',
|
||||
'bg-white shadow-md hover:shadow-xl hover:-translate-y-1',
|
||||
featureItemStyle.classNames
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
style={featureItemStyle.style}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
)}
|
||||
{/* Thumbnail */}
|
||||
{item.featured_image ? (
|
||||
<div className="aspect-[16/9] overflow-hidden bg-gray-100">
|
||||
<img
|
||||
src={item.featured_image}
|
||||
alt={item.title || ''}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aspect-[16/9] bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center">
|
||||
<svg className="w-10 h-10 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.description && (
|
||||
<p className={cn(
|
||||
'wn-feature-grid__item-desc',
|
||||
!featureItemStyle.style?.color && {
|
||||
'text-gray-600': colorScheme !== 'primary',
|
||||
'text-white/80': colorScheme === 'primary',
|
||||
}
|
||||
{/* Card Body */}
|
||||
<div className="p-5">
|
||||
{item.date && (
|
||||
<p className="text-xs text-gray-400 mb-2 uppercase tracking-wider">{item.date}</p>
|
||||
)}
|
||||
{item.title && (
|
||||
<h3 className="font-semibold text-gray-900 text-base leading-snug mb-2 group-hover:text-primary transition-colors line-clamp-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
)}
|
||||
{(item.excerpt || item.description) && (
|
||||
<p className="text-sm text-gray-500 line-clamp-3 mb-4">
|
||||
{item.excerpt || item.description}
|
||||
</p>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-1 text-sm font-medium text-primary">
|
||||
Read more
|
||||
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Feature Card (icon + title + desc) ─────────────────────
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'wn-feature-grid__item',
|
||||
'p-6 rounded-xl',
|
||||
!featureItemStyle.style?.backgroundColor && {
|
||||
'bg-white shadow-lg': colorScheme !== 'primary',
|
||||
'bg-white/10': colorScheme === 'primary',
|
||||
},
|
||||
featureItemStyle.classNames
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
style={featureItemStyle.style}
|
||||
>
|
||||
{item.icon && (() => {
|
||||
const IconComponent = (LucideIcons as any)[item.icon];
|
||||
if (!IconComponent) return null;
|
||||
return (
|
||||
<div className="wn-feature-grid__icon mb-4 inline-block p-3 rounded-full bg-primary/10 text-primary">
|
||||
<IconComponent className="w-8 h-8" />
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{item.title && (
|
||||
<h3
|
||||
className={cn(
|
||||
"wn-feature-grid__item-title mb-3",
|
||||
!featureItemStyle.classNames && "text-xl font-semibold"
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
)}
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn(
|
||||
'wn-feature-grid__item-desc',
|
||||
!featureItemStyle.style?.color && {
|
||||
'text-gray-600': colorScheme !== 'primary',
|
||||
'text-white/80': colorScheme === 'primary',
|
||||
}
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Empty state for related posts */}
|
||||
{isPostCards && listItems.length === 0 && (
|
||||
<p className="text-center text-gray-400 text-sm py-8">No related articles found.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user