Fix button roundtrip in editor, alignment persistence, and test email rendering
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
type: string;
|
||||
layoutVariant?: string;
|
||||
colorScheme?: string;
|
||||
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
||||
elementStyles?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface CTABannerRendererProps {
|
||||
section: Section;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const COLOR_SCHEMES: Record<string, { bg: string; text: string; btnBg: string; btnText: string }> = {
|
||||
default: { bg: '', text: 'text-gray-900', btnBg: 'bg-blue-600', btnText: 'text-white' },
|
||||
primary: { bg: 'bg-blue-600', text: 'text-white', btnBg: 'bg-white', btnText: 'text-blue-600' },
|
||||
secondary: { bg: 'bg-gray-800', text: 'text-white', btnBg: 'bg-white', btnText: 'text-gray-800' },
|
||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700', btnBg: 'bg-gray-800', btnText: 'text-white' },
|
||||
gradient: { bg: 'bg-gradient-to-r from-purple-600 to-blue-500', text: 'text-white', btnBg: 'bg-white', btnText: 'text-purple-600' },
|
||||
};
|
||||
|
||||
export function CTABannerRenderer({ section, className }: CTABannerRendererProps) {
|
||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'primary'];
|
||||
|
||||
const title = section.props?.title?.value || 'Ready to get started?';
|
||||
const text = section.props?.text?.value || 'Join thousands of happy customers today.';
|
||||
const buttonText = section.props?.button_text?.value || 'Get Started';
|
||||
const buttonUrl = section.props?.button_url?.value || '#';
|
||||
|
||||
// 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 titleStyle = getTextStyles('title');
|
||||
const textStyle = getTextStyles('text');
|
||||
const btnStyle = getTextStyles('button_text');
|
||||
|
||||
return (
|
||||
<div className={cn('py-12 px-4 md:py-20 md:px-8', scheme.bg, scheme.text, className)}>
|
||||
<div className="max-w-4xl mx-auto text-center space-y-6">
|
||||
<h2
|
||||
className={cn(
|
||||
!titleStyle.classNames && "text-3xl md:text-4xl font-bold",
|
||||
titleStyle.classNames
|
||||
)}
|
||||
style={titleStyle.style}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className={cn(
|
||||
"max-w-2xl mx-auto",
|
||||
!textStyle.classNames && "text-lg opacity-90",
|
||||
textStyle.classNames
|
||||
)}
|
||||
style={textStyle.style}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
<button className={cn(
|
||||
'inline-flex items-center gap-2 px-8 py-4 rounded-lg font-semibold transition hover:opacity-90',
|
||||
!btnStyle.style?.backgroundColor && scheme.btnBg,
|
||||
!btnStyle.style?.color && scheme.btnText,
|
||||
btnStyle.classNames
|
||||
)}
|
||||
style={btnStyle.style}
|
||||
>
|
||||
{buttonText}
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Send, Mail, User, MessageSquare } from 'lucide-react';
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
type: string;
|
||||
layoutVariant?: string;
|
||||
colorScheme?: string;
|
||||
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
||||
elementStyles?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface ContactFormRendererProps {
|
||||
section: Section;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const COLOR_SCHEMES: Record<string, { bg: string; text: string; inputBg: string; btnBg: string }> = {
|
||||
default: { bg: '', text: 'text-gray-900', inputBg: 'bg-gray-50', btnBg: 'bg-blue-600' },
|
||||
primary: { bg: 'wn-primary-bg', text: 'text-white', inputBg: 'bg-white', btnBg: 'bg-white' },
|
||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white', inputBg: 'bg-gray-700', btnBg: 'bg-blue-500' },
|
||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700', inputBg: 'bg-white', btnBg: 'bg-gray-800' },
|
||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white', inputBg: 'bg-white/90', btnBg: 'bg-white' },
|
||||
};
|
||||
|
||||
export function ContactFormRenderer({ section, className }: ContactFormRendererProps) {
|
||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
||||
|
||||
const title = section.props?.title?.value || 'Contact Us';
|
||||
|
||||
// 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
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const titleStyle = getTextStyles('title');
|
||||
const buttonStyleObj = getTextStyles('button');
|
||||
const fieldsStyleObj = getTextStyles('fields');
|
||||
|
||||
const buttonStyle = section.elementStyles?.button || {};
|
||||
const fieldsStyle = section.elementStyles?.fields || {};
|
||||
|
||||
// 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-xl mx-auto">
|
||||
<h2
|
||||
className={cn(
|
||||
"text-3xl font-bold text-center mb-8",
|
||||
titleStyle.classNames
|
||||
)}
|
||||
style={titleStyle.style}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||
{/* Name field */}
|
||||
<div className="relative">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Your Name"
|
||||
className={cn(
|
||||
'w-full pl-12 pr-4 py-3 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
!fieldsStyle.backgroundColor && scheme.inputBg
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: fieldsStyle.backgroundColor,
|
||||
color: fieldsStyle.color
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email field */}
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Your Email"
|
||||
className={cn(
|
||||
'w-full pl-12 pr-4 py-3 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
!fieldsStyle.backgroundColor && scheme.inputBg
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: fieldsStyle.backgroundColor,
|
||||
color: fieldsStyle.color
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Message field */}
|
||||
<div className="relative">
|
||||
<MessageSquare className="absolute left-4 top-4 w-5 h-5 text-gray-400" />
|
||||
<textarea
|
||||
placeholder="Your Message"
|
||||
rows={4}
|
||||
className={cn(
|
||||
'w-full pl-12 pr-4 py-3 rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none',
|
||||
!fieldsStyle.backgroundColor && scheme.inputBg
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: fieldsStyle.backgroundColor,
|
||||
color: fieldsStyle.color
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit button */}
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(
|
||||
'w-full flex items-center justify-center gap-2 py-3 rounded-lg font-semibold transition opacity-80 cursor-not-allowed',
|
||||
!buttonStyle.backgroundColor && scheme.btnBg,
|
||||
!buttonStyle.color && (section.colorScheme === 'primary' || section.colorScheme === 'gradient' ? 'text-blue-600' : 'text-white'),
|
||||
buttonStyleObj.classNames
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: buttonStyle.backgroundColor,
|
||||
color: buttonStyle.color
|
||||
}}
|
||||
disabled
|
||||
>
|
||||
<Send className="w-5 h-5" />
|
||||
Send Message
|
||||
</button>
|
||||
|
||||
<p className="text-center text-sm opacity-60">
|
||||
(Form preview only - functional on frontend)
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Section } from '../../store/usePageEditorStore';
|
||||
|
||||
interface ContentRendererProps {
|
||||
section: Section;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
||||
default: { bg: 'bg-white', text: 'text-gray-900' },
|
||||
light: { bg: 'bg-gray-50', text: 'text-gray-900' },
|
||||
dark: { bg: 'bg-gray-900', text: 'text-white' },
|
||||
blue: { bg: 'wn-primary-bg', text: 'text-white' },
|
||||
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white' },
|
||||
};
|
||||
|
||||
const WIDTH_CLASSES: Record<string, string> = {
|
||||
default: 'max-w-6xl',
|
||||
narrow: 'max-w-2xl',
|
||||
medium: 'max-w-4xl',
|
||||
};
|
||||
|
||||
const fontSizeToCSS = (className?: string) => {
|
||||
switch (className) {
|
||||
case 'text-sm': return '0.875rem';
|
||||
case 'text-base': return '1rem';
|
||||
case 'text-lg': return '1.125rem';
|
||||
case 'text-xl': return '1.25rem';
|
||||
case 'text-2xl': return '1.5rem';
|
||||
case 'text-3xl': return '1.875rem';
|
||||
case 'text-4xl': return '2.25rem';
|
||||
case 'text-5xl': return '3rem';
|
||||
case 'text-6xl': return '3.75rem';
|
||||
default: return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const fontWeightToCSS = (className?: string) => {
|
||||
switch (className) {
|
||||
case 'font-light': return '300';
|
||||
case 'font-normal': return '400';
|
||||
case 'font-medium': return '500';
|
||||
case 'font-semibold': return '600';
|
||||
case 'font-bold': return '700';
|
||||
case 'font-extrabold': return '800';
|
||||
default: return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to generate scoped CSS for prose elements
|
||||
const generateScopedStyles = (sectionId: string, elementStyles: Record<string, any>) => {
|
||||
const styles: string[] = [];
|
||||
const scope = `#section-${sectionId}`;
|
||||
|
||||
// Headings (h1-h4)
|
||||
const hs = elementStyles?.heading;
|
||||
if (hs) {
|
||||
const headingRules = [
|
||||
hs.color && `color: ${hs.color} !important;`,
|
||||
hs.fontWeight && `font-weight: ${fontWeightToCSS(hs.fontWeight)} !important;`,
|
||||
hs.fontFamily && `font-family: var(--font-${hs.fontFamily}, inherit) !important;`,
|
||||
hs.fontSize && `font-size: ${fontSizeToCSS(hs.fontSize)} !important;`,
|
||||
hs.backgroundColor && `background-color: ${hs.backgroundColor} !important;`,
|
||||
// Add padding if background color is set to make it look decent
|
||||
hs.backgroundColor && `padding: 0.2em 0.4em; border-radius: 4px; display: inline-block;`,
|
||||
].filter(Boolean).join(' ');
|
||||
if (headingRules) {
|
||||
styles.push(`${scope} h1, ${scope} h2, ${scope} h3, ${scope} h4 { ${headingRules} }`);
|
||||
}
|
||||
}
|
||||
|
||||
// Body text (p, li)
|
||||
const ts = elementStyles?.text;
|
||||
if (ts) {
|
||||
const textRules = [
|
||||
ts.color && `color: ${ts.color} !important;`,
|
||||
ts.fontSize && `font-size: ${fontSizeToCSS(ts.fontSize)} !important;`,
|
||||
ts.fontWeight && `font-weight: ${fontWeightToCSS(ts.fontWeight)} !important;`,
|
||||
ts.fontFamily && `font-family: var(--font-${ts.fontFamily}, inherit) !important;`,
|
||||
].filter(Boolean).join(' ');
|
||||
if (textRules) {
|
||||
styles.push(`${scope} p, ${scope} li, ${scope} ul, ${scope} ol { ${textRules} }`);
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit Spacing & List Formatting Restorations
|
||||
// These ensure vertical rhythm and list styles exist even if prose defaults are overridden or missing
|
||||
styles.push(`
|
||||
${scope} p { margin-bottom: 1em; }
|
||||
${scope} h1, ${scope} h2, ${scope} h3, ${scope} h4 { margin-top: 1.5em; margin-bottom: 0.5em; line-height: 1.2; }
|
||||
${scope} ul { list-style-type: disc; padding-left: 1.5em; margin-bottom: 1em; }
|
||||
${scope} ol { list-style-type: decimal; padding-left: 1.5em; margin-bottom: 1em; }
|
||||
${scope} li { margin-bottom: 0.25em; }
|
||||
${scope} img { margin-top: 1.5em; margin-bottom: 1.5em; }
|
||||
`);
|
||||
|
||||
// Links (a:not(.button))
|
||||
const ls = elementStyles?.link;
|
||||
if (ls) {
|
||||
const linkRules = [
|
||||
ls.color && `color: ${ls.color} !important;`,
|
||||
ls.textDecoration && `text-decoration: ${ls.textDecoration} !important;`,
|
||||
ls.fontSize && `font-size: ${fontSizeToCSS(ls.fontSize)} !important;`,
|
||||
ls.fontWeight && `font-weight: ${fontWeightToCSS(ls.fontWeight)} !important;`,
|
||||
].filter(Boolean).join(' ');
|
||||
if (linkRules) {
|
||||
styles.push(`${scope} a:not([data-button]):not(.button) { ${linkRules} }`);
|
||||
}
|
||||
if (ls.hoverColor) {
|
||||
styles.push(`${scope} a:not([data-button]):not(.button):hover { color: ${ls.hoverColor} !important; }`);
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons (a[data-button], .button)
|
||||
const bs = elementStyles?.button;
|
||||
if (bs) {
|
||||
const btnRules = [
|
||||
bs.backgroundColor && `background-color: ${bs.backgroundColor} !important;`,
|
||||
bs.color && `color: ${bs.color} !important;`,
|
||||
bs.borderRadius && `border-radius: ${bs.borderRadius} !important;`,
|
||||
bs.padding && `padding: ${bs.padding} !important;`,
|
||||
bs.fontSize && `font-size: ${fontSizeToCSS(bs.fontSize)} !important;`,
|
||||
bs.fontWeight && `font-weight: ${fontWeightToCSS(bs.fontWeight)} !important;`,
|
||||
bs.borderColor && `border: ${bs.borderWidth || '1px'} solid ${bs.borderColor} !important;`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
// Always force text-decoration: none for buttons
|
||||
styles.push(`${scope} a[data-button], ${scope} .button { ${btnRules} display: inline-block; text-decoration: none !important; }`);
|
||||
// Add hover effect opacity or something to make it feel alive, or just keep it simple
|
||||
styles.push(`${scope} a[data-button]:hover, ${scope} .button:hover { opacity: 0.9; }`);
|
||||
}
|
||||
|
||||
// Images
|
||||
const is = elementStyles?.image;
|
||||
if (is) {
|
||||
const imgRules = [
|
||||
is.objectFit && `object-fit: ${is.objectFit} !important;`,
|
||||
is.borderRadius && `border-radius: ${is.borderRadius} !important;`,
|
||||
is.width && `width: ${is.width} !important;`,
|
||||
is.height && `height: ${is.height} !important;`,
|
||||
].filter(Boolean).join(' ');
|
||||
if (imgRules) {
|
||||
styles.push(`${scope} img { ${imgRules} }`);
|
||||
}
|
||||
}
|
||||
|
||||
return styles.join('\n');
|
||||
};
|
||||
|
||||
export function ContentRenderer({ section, className }: ContentRendererProps) {
|
||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
||||
const layout = section.layoutVariant || 'default';
|
||||
const widthClass = WIDTH_CLASSES[layout] || WIDTH_CLASSES.default;
|
||||
|
||||
const heightPreset = section.styles?.heightPreset || 'default';
|
||||
|
||||
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-32',
|
||||
'screen': 'min-h-screen py-20 flex items-center',
|
||||
};
|
||||
|
||||
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-20';
|
||||
|
||||
const content = section.props?.content?.value || 'Your content goes here. Edit this in the inspector panel.';
|
||||
const isDynamic = section.props?.content?.type === 'dynamic';
|
||||
|
||||
// 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 contentStyle = getTextStyles('content');
|
||||
const headingStyle = getTextStyles('heading');
|
||||
const buttonStyle = getTextStyles('button');
|
||||
const cta_text = section.props?.cta_text?.value;
|
||||
const cta_url = section.props?.cta_url?.value;
|
||||
|
||||
// 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
|
||||
id={`section-${section.id}`}
|
||||
className={cn(
|
||||
'px-4 md:px-8',
|
||||
heightClasses,
|
||||
!scheme.bg.startsWith('wn-') && scheme.bg,
|
||||
scheme.text,
|
||||
className
|
||||
)}
|
||||
style={getBackgroundStyle()}
|
||||
>
|
||||
<style dangerouslySetInnerHTML={{ __html: generateScopedStyles(section.id, section.elementStyles || {}) }} />
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'mx-auto prose prose-lg max-w-none',
|
||||
// Default prose overrides
|
||||
'prose-headings:text-[var(--tw-prose-headings)]',
|
||||
'prose-a:no-underline hover:prose-a:underline', // Establish baseline for links
|
||||
widthClass,
|
||||
scheme.text === 'text-white' && 'prose-invert',
|
||||
contentStyle.classNames // Apply font family, size, weight to container just in case
|
||||
)}
|
||||
style={{
|
||||
color: contentStyle.style.color,
|
||||
textAlign: contentStyle.style.textAlign as React.CSSProperties['textAlign'],
|
||||
'--tw-prose-headings': headingStyle.style?.color,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{isDynamic && (
|
||||
<div className="flex items-center gap-2 text-orange-400 text-sm font-medium mb-4">
|
||||
<span>◆</span>
|
||||
<span>{section.props?.content?.source || 'Dynamic Content'}</span>
|
||||
</div>
|
||||
)}
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
|
||||
{cta_text && cta_url && (
|
||||
<div className="mt-8">
|
||||
<a
|
||||
href={cta_url}
|
||||
className={cn(
|
||||
"button inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2",
|
||||
!buttonStyle.style?.backgroundColor && "bg-blue-600",
|
||||
!buttonStyle.style?.color && "text-white",
|
||||
buttonStyle.classNames
|
||||
)}
|
||||
style={buttonStyle.style}
|
||||
>
|
||||
{cta_text}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
type: string;
|
||||
layoutVariant?: string;
|
||||
colorScheme?: string;
|
||||
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
||||
elementStyles?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface HeroRendererProps {
|
||||
section: Section;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
||||
default: { bg: '', text: 'text-gray-900' },
|
||||
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white' },
|
||||
};
|
||||
|
||||
export function HeroRenderer({ section, className }: HeroRendererProps) {
|
||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
||||
const layout = section.layoutVariant || 'default';
|
||||
|
||||
const title = section.props?.title?.value || 'Hero Title';
|
||||
const subtitle = section.props?.subtitle?.value || 'Your amazing subtitle here';
|
||||
const image = section.props?.image?.value;
|
||||
const ctaText = section.props?.cta_text?.value || 'Get Started';
|
||||
const ctaUrl = section.props?.cta_url?.value || '#';
|
||||
|
||||
// Check for dynamic placeholders
|
||||
const isDynamicTitle = section.props?.title?.type === 'dynamic';
|
||||
const isDynamicSubtitle = section.props?.subtitle?.type === 'dynamic';
|
||||
const isDynamicImage = section.props?.image?.type === 'dynamic';
|
||||
|
||||
// Element Styles
|
||||
// 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', // Mapping secondary to sans for now if primary is assumed default
|
||||
'font-serif': styles.fontFamily === 'primary', // Mapping primary to serif (headings) - ADJUST AS NEEDED
|
||||
}
|
||||
),
|
||||
style: {
|
||||
color: styles.color,
|
||||
backgroundColor: styles.backgroundColor,
|
||||
textAlign: styles.textAlign
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const titleStyle = getTextStyles('title');
|
||||
const subtitleStyle = getTextStyles('subtitle');
|
||||
const ctaStyle = getTextStyles('cta_text'); // For button
|
||||
|
||||
// Helper for image styles
|
||||
const imageStyle = section.elementStyles?.['image'] || {};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
if (layout === 'hero-left-image' || layout === 'hero-right-image') {
|
||||
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={cn(
|
||||
'max-w-6xl mx-auto flex items-center gap-12',
|
||||
layout === 'hero-right-image' ? 'flex-col md:flex-row-reverse' : 'flex-col md:flex-row',
|
||||
'flex-wrap md:flex-nowrap'
|
||||
)}>
|
||||
{/* Image */}
|
||||
<div className="w-full md:w-1/2">
|
||||
<div
|
||||
className="rounded-lg shadow-xl overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: imageStyle.backgroundColor,
|
||||
width: imageStyle.width || 'auto',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
>
|
||||
{image ? (
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="w-full h-auto block"
|
||||
style={{
|
||||
objectFit: imageStyle.objectFit,
|
||||
height: imageStyle.height,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-64 md:h-80 bg-gray-300 flex items-center justify-center">
|
||||
<span className="text-gray-500">
|
||||
{isDynamicImage ? `◆ ${section.props?.image?.source}` : 'No Image'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="w-full md:w-1/2 space-y-4">
|
||||
<h1
|
||||
className={cn("font-bold", titleStyle.classNames || "text-3xl md:text-5xl")}
|
||||
style={titleStyle.style}
|
||||
>
|
||||
{isDynamicTitle && <span className="text-orange-400 mr-2">◆</span>}
|
||||
{title}
|
||||
</h1>
|
||||
<p
|
||||
className={cn("opacity-90", subtitleStyle.classNames || "text-lg md:text-xl")}
|
||||
style={subtitleStyle.style}
|
||||
>
|
||||
{isDynamicSubtitle && <span className="text-orange-400 mr-2">◆</span>}
|
||||
{subtitle}
|
||||
</p>
|
||||
{ctaText && (
|
||||
<button
|
||||
className={cn(
|
||||
"px-6 py-3 rounded-lg transition hover:opacity-90",
|
||||
!ctaStyle.style?.backgroundColor && "bg-white",
|
||||
!ctaStyle.style?.color && "text-gray-900",
|
||||
ctaStyle.classNames
|
||||
)}
|
||||
style={ctaStyle.style}
|
||||
>
|
||||
{ctaText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default centered layout
|
||||
return (
|
||||
<div
|
||||
className={cn('py-12 px-4 md:py-20 md:px-8 text-center', !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
||||
style={getBackgroundStyle()}
|
||||
>
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<h1
|
||||
className={cn("font-bold", titleStyle.classNames || "text-4xl md:text-6xl")}
|
||||
style={titleStyle.style}
|
||||
>
|
||||
{isDynamicTitle && <span className="text-orange-400 mr-2">◆</span>}
|
||||
{title}
|
||||
</h1>
|
||||
<p
|
||||
className={cn("opacity-90 max-w-2xl mx-auto", subtitleStyle.classNames || "text-lg md:text-2xl")}
|
||||
style={subtitleStyle.style}
|
||||
>
|
||||
{isDynamicSubtitle && <span className="text-orange-400 mr-2">◆</span>}
|
||||
{subtitle}
|
||||
</p>
|
||||
|
||||
{/* Image with Wrapper for Background */}
|
||||
<div
|
||||
className={cn("mx-auto", imageStyle.width ? "" : "max-w-3xl w-full")}
|
||||
style={{ backgroundColor: imageStyle.backgroundColor, width: imageStyle.width || 'auto', maxWidth: '100%' }}
|
||||
>
|
||||
{image ? (
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className={cn(
|
||||
"w-full rounded-xl shadow-lg mt-8",
|
||||
!imageStyle.height && "h-auto", // Default height if not specified
|
||||
!imageStyle.objectFit && "object-cover" // Default fit if not specified
|
||||
)}
|
||||
style={{
|
||||
objectFit: imageStyle.objectFit,
|
||||
height: imageStyle.height,
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
/>
|
||||
) : isDynamicImage ? (
|
||||
<div className="w-full h-64 bg-gray-300 rounded-xl flex items-center justify-center mt-8">
|
||||
<span className="text-gray-500">◆ {section.props?.image?.source}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{ctaText && (
|
||||
<button
|
||||
className={cn(
|
||||
"px-8 py-4 rounded-lg transition hover:opacity-90 mt-4",
|
||||
!ctaStyle.style?.backgroundColor && "bg-white",
|
||||
!ctaStyle.style?.color && "text-gray-900",
|
||||
ctaStyle.classNames || "font-semibold"
|
||||
)}
|
||||
style={ctaStyle.style}
|
||||
>
|
||||
{ctaText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Section } from '../../store/usePageEditorStore';
|
||||
|
||||
|
||||
|
||||
interface ImageTextRendererProps {
|
||||
section: Section;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
||||
default: { bg: '', text: 'text-gray-900' },
|
||||
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white' },
|
||||
};
|
||||
|
||||
export function ImageTextRenderer({ section, className }: ImageTextRendererProps) {
|
||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
||||
const layout = section.layoutVariant || 'image-left';
|
||||
const isImageRight = layout === 'image-right';
|
||||
|
||||
const title = section.props?.title?.value || 'Section Title';
|
||||
const text = section.props?.text?.value || 'Your descriptive text goes here. Edit this section to add your own content.';
|
||||
const image = section.props?.image?.value;
|
||||
|
||||
const isDynamicTitle = section.props?.title?.type === 'dynamic';
|
||||
const isDynamicText = section.props?.text?.type === 'dynamic';
|
||||
const isDynamicImage = section.props?.image?.type === 'dynamic';
|
||||
|
||||
const cta_text = section.props?.cta_text?.value;
|
||||
const cta_url = section.props?.cta_url?.value;
|
||||
|
||||
// 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 titleStyle = getTextStyles('title');
|
||||
const textStyle = getTextStyles('text');
|
||||
const imageStyle = section.elementStyles?.['image'] || {};
|
||||
|
||||
const buttonStyle = getTextStyles('button');
|
||||
|
||||
// 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={cn(
|
||||
'max-w-6xl mx-auto flex items-center gap-12',
|
||||
isImageRight ? 'flex-col md:flex-row-reverse' : 'flex-col md:flex-row',
|
||||
'flex-wrap md:flex-nowrap'
|
||||
)}>
|
||||
{/* Image */}
|
||||
<div className="w-full md:w-1/2" style={{ backgroundColor: imageStyle.backgroundColor }}>
|
||||
{image ? (
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="w-full h-auto rounded-xl shadow-lg"
|
||||
style={{
|
||||
objectFit: imageStyle.objectFit,
|
||||
width: imageStyle.width,
|
||||
maxWidth: '100%',
|
||||
height: imageStyle.height,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-64 md:h-80 bg-gray-200 rounded-xl flex items-center justify-center">
|
||||
<span className="text-gray-400">
|
||||
{isDynamicImage ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-orange-400">◆</span>
|
||||
{section.props?.image?.source}
|
||||
</span>
|
||||
) : (
|
||||
'Add Image'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="w-full md:w-1/2 space-y-4">
|
||||
<h2
|
||||
className={cn(
|
||||
"text-2xl md:text-3xl font-bold",
|
||||
titleStyle.classNames
|
||||
)}
|
||||
style={titleStyle.style}
|
||||
>
|
||||
{isDynamicTitle && <span className="text-orange-400 mr-2">◆</span>}
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className={cn(
|
||||
"text-lg opacity-90 leading-relaxed",
|
||||
textStyle.classNames
|
||||
)}
|
||||
style={textStyle.style}
|
||||
>
|
||||
{isDynamicText && <span className="text-orange-400 mr-2">◆</span>}
|
||||
{text}
|
||||
</p>
|
||||
|
||||
{cta_text && cta_url && (
|
||||
<div className="pt-4">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium h-10 px-4 py-2",
|
||||
!buttonStyle.style?.backgroundColor && "bg-blue-600",
|
||||
!buttonStyle.style?.color && "text-white",
|
||||
buttonStyle.classNames
|
||||
)}
|
||||
style={buttonStyle.style}
|
||||
>
|
||||
{cta_text}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { HeroRenderer } from './HeroRenderer';
|
||||
export { ContentRenderer } from './ContentRenderer';
|
||||
export { ImageTextRenderer } from './ImageTextRenderer';
|
||||
export { FeatureGridRenderer } from './FeatureGridRenderer';
|
||||
export { CTABannerRenderer } from './CTABannerRenderer';
|
||||
export { ContactFormRenderer } from './ContactFormRenderer';
|
||||
Reference in New Issue
Block a user