Fix button roundtrip in editor, alignment persistence, and test email rendering

This commit is contained in:
Dwindi Ramadhana
2026-01-17 13:10:50 +07:00
parent 0e9ace902d
commit 6d2136d3b5
61 changed files with 8287 additions and 866 deletions

View File

@@ -0,0 +1,145 @@
import * as LucideIcons from 'lucide-react';
import { cn } from '@/lib/utils';
interface Section {
id: string;
type: string;
layoutVariant?: string;
colorScheme?: string;
props: Record<string, any>;
elementStyles?: Record<string, any>;
}
interface FeatureGridRendererProps {
section: Section;
className?: string;
}
const COLOR_SCHEMES: Record<string, { bg: string; text: string; cardBg: string }> = {
default: { bg: '', text: 'text-gray-900', cardBg: 'bg-gray-50' },
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> = {
'grid-2': 'grid-cols-1 md:grid-cols-2',
'grid-3': 'grid-cols-1 md:grid-cols-3',
'grid-4': 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
};
// Default features for 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' },
];
export function FeatureGridRenderer({ section, className }: FeatureGridRendererProps) {
const scheme = COLOR_SCHEMES[section.colorScheme || '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;
// Helper to get text styles (including font family)
const getTextStyles = (elementName: string) => {
const styles = section.elementStyles?.[elementName] || {};
return {
classNames: cn(
styles.fontSize,
styles.fontWeight,
{
'font-sans': styles.fontFamily === 'secondary',
'font-serif': styles.fontFamily === 'primary',
}
),
style: {
color: styles.color,
textAlign: styles.textAlign,
backgroundColor: styles.backgroundColor
}
};
};
const headingStyle = getTextStyles('heading');
const featureItemStyle = getTextStyles('feature_item');
// Helper to get background style for dynamic schemes
const getBackgroundStyle = () => {
if (scheme.bg === 'wn-gradient-bg') {
return { backgroundImage: 'linear-gradient(135deg, var(--wn-gradient-start, #9333ea), var(--wn-gradient-end, #3b82f6))' };
}
if (scheme.bg === 'wn-primary-bg') {
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
}
if (scheme.bg === 'wn-secondary-bg') {
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
}
return undefined;
};
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()}
>
<div className="max-w-6xl mx-auto">
{heading && (
<h2
className={cn(
"text-3xl md:text-4xl font-bold text-center mb-12",
headingStyle.classNames
)}
style={headingStyle.style}
>
{heading}
</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" />
</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>
);
}