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,5 +1,6 @@
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Image, Calendar, User } from 'lucide-react';
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
@@ -8,6 +9,7 @@ interface Section {
|
||||
colorScheme?: string;
|
||||
props: Record<string, any>;
|
||||
elementStyles?: Record<string, any>;
|
||||
styles?: any;
|
||||
}
|
||||
|
||||
interface FeatureGridRendererProps {
|
||||
@@ -20,7 +22,6 @@ const COLOR_SCHEMES: Record<string, { bg: string; text: string; cardBg: string }
|
||||
primary: { bg: 'wn-primary-bg', text: 'text-white', cardBg: 'bg-white/10' },
|
||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white', cardBg: 'bg-white/10' },
|
||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700', cardBg: 'bg-white' },
|
||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white', cardBg: 'bg-white/10' },
|
||||
};
|
||||
|
||||
const GRID_CLASSES: Record<string, string> = {
|
||||
@@ -29,20 +30,57 @@ const GRID_CLASSES: Record<string, string> = {
|
||||
'grid-4': 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
||||
};
|
||||
|
||||
// Default features for demo
|
||||
// Default features for static demo
|
||||
const DEFAULT_FEATURES = [
|
||||
{ title: 'Fast Delivery', description: 'Quick shipping to your doorstep', icon: 'Truck' },
|
||||
{ title: 'Secure Payment', description: 'Your data is always protected', icon: 'Shield' },
|
||||
{ title: 'Quality Products', description: 'Only the best for our customers', icon: 'Star' },
|
||||
];
|
||||
|
||||
// Placeholder post-card skeleton shown when features are dynamic (related_posts)
|
||||
function PostCardPlaceholder({ index, cardBg }: { index: number; cardBg: string }) {
|
||||
const widths = ['w-3/4', 'w-2/3', 'w-4/5'];
|
||||
const titleWidth = widths[index % widths.length];
|
||||
return (
|
||||
<div className={cn('rounded-xl overflow-hidden', cardBg, 'shadow-sm border border-dashed border-gray-300')}>
|
||||
{/* Thumbnail placeholder */}
|
||||
<div className="aspect-[16/9] bg-gray-200 flex items-center justify-center">
|
||||
<Image className="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
<div className="p-4 space-y-2">
|
||||
{/* Meta row */}
|
||||
<div className="flex items-center gap-3 text-xs text-gray-400">
|
||||
<span className="flex items-center gap-1"><Calendar className="w-3 h-3" /> Jan 1, 2025</span>
|
||||
<span className="flex items-center gap-1"><User className="w-3 h-3" /> Author</span>
|
||||
</div>
|
||||
{/* Title skeleton */}
|
||||
<div className={cn('h-4 bg-gray-300 rounded animate-pulse', titleWidth)} />
|
||||
{/* Excerpt skeleton */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="h-3 bg-gray-200 rounded animate-pulse w-full" />
|
||||
<div className="h-3 bg-gray-200 rounded animate-pulse w-5/6" />
|
||||
</div>
|
||||
{/* "Read more" chip */}
|
||||
<div className="pt-1">
|
||||
<div className="inline-block h-3 w-16 bg-blue-200 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeatureGridRenderer({ section, className }: FeatureGridRendererProps) {
|
||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'] ?? COLOR_SCHEMES['default'];
|
||||
const layout = section.layoutVariant || 'grid-3';
|
||||
const gridClass = GRID_CLASSES[layout] || GRID_CLASSES['grid-3'];
|
||||
|
||||
const heading = section.props?.heading?.value || 'Our Features';
|
||||
const features = section.props?.features?.value || DEFAULT_FEATURES;
|
||||
const featuresProp = section.props?.features;
|
||||
const isDynamic = featuresProp?.type === 'dynamic' && !!featuresProp?.source;
|
||||
const features = isDynamic ? [] : (featuresProp?.value || DEFAULT_FEATURES);
|
||||
|
||||
// Determine how many placeholder post-cards to show (match grid columns)
|
||||
const placeholderCount = layout === 'grid-4' ? 4 : layout === 'grid-2' ? 2 : 3;
|
||||
|
||||
// Helper to get text styles (including font family)
|
||||
const getTextStyles = (elementName: string) => {
|
||||
@@ -81,10 +119,22 @@ export function FeatureGridRenderer({ section, className }: FeatureGridRendererP
|
||||
return undefined;
|
||||
};
|
||||
|
||||
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-[50vh] flex flex-col justify-center',
|
||||
};
|
||||
const customPadding = section.styles?.paddingTop || section.styles?.paddingBottom;
|
||||
const heightClasses = customPadding ? '' : (heightMap[section.styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||
|
||||
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('py-12 px-4 md:py-20 md:px-8', !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
||||
style={getBackgroundStyle()}
|
||||
className={cn(heightClasses, 'px-4 md:px-8', !hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
||||
style={hasCustomBackground ? {} : getBackgroundStyle()}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{heading && (
|
||||
@@ -99,47 +149,56 @@ export function FeatureGridRenderer({ section, className }: FeatureGridRendererP
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<div className={cn('grid gap-8', gridClass)}>
|
||||
{(Array.isArray(features) ? features : DEFAULT_FEATURES).map((feature: any, index: number) => {
|
||||
// Resolve icon from name, fallback to Star
|
||||
const IconComponent = (LucideIcons as any)[feature.icon] || LucideIcons.Star;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'p-6 rounded-xl text-center',
|
||||
!featureItemStyle.style?.backgroundColor && scheme.cardBg,
|
||||
featureItemStyle.classNames
|
||||
)}
|
||||
style={featureItemStyle.style}
|
||||
>
|
||||
<div className="w-14 h-14 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<IconComponent className="w-7 h-7 text-blue-600" />
|
||||
{/* Dynamic (related posts) — show post-card skeleton placeholders */}
|
||||
{isDynamic ? (
|
||||
<div className={cn('grid gap-8', gridClass)}>
|
||||
{Array.from({ length: placeholderCount }).map((_, i) => (
|
||||
<PostCardPlaceholder key={i} index={i} cardBg={scheme.cardBg} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* Static items — regular icon feature cards */
|
||||
<div className={cn('grid gap-8', gridClass)}>
|
||||
{(Array.isArray(features) ? features : DEFAULT_FEATURES).map((feature: any, index: number) => {
|
||||
const IconComponent = (LucideIcons as any)[feature.icon] || LucideIcons.Star;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'p-6 rounded-xl text-center',
|
||||
!featureItemStyle.style?.backgroundColor && scheme.cardBg,
|
||||
featureItemStyle.classNames
|
||||
)}
|
||||
style={featureItemStyle.style}
|
||||
>
|
||||
<div className="w-14 h-14 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<IconComponent className="w-7 h-7 text-blue-600" />
|
||||
</div>
|
||||
<h3
|
||||
className={cn(
|
||||
"mb-2",
|
||||
!featureItemStyle.style?.color && "text-lg font-semibold"
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
>
|
||||
{feature.title || `Feature ${index + 1}`}
|
||||
</h3>
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm",
|
||||
!featureItemStyle.style?.color && "opacity-80"
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
>
|
||||
{feature.description || 'Feature description goes here'}
|
||||
</p>
|
||||
</div>
|
||||
<h3
|
||||
className={cn(
|
||||
"mb-2",
|
||||
!featureItemStyle.style?.color && "text-lg font-semibold"
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
>
|
||||
{feature.title || `Feature ${index + 1}`}
|
||||
</h3>
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm",
|
||||
!featureItemStyle.style?.color && "opacity-80"
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
>
|
||||
{feature.description || 'Feature description goes here'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user