Fix button roundtrip in editor, alignment persistence, and test email rendering
This commit is contained in:
@@ -56,22 +56,42 @@ const getAppearanceSettings = () => {
|
||||
return (window as any).woonoowCustomer?.appearanceSettings || {};
|
||||
};
|
||||
|
||||
// Get initial route from data attribute (set by PHP based on SPA mode)
|
||||
// Get initial route from data attribute or derive from SPA mode
|
||||
const getInitialRoute = () => {
|
||||
const appEl = document.getElementById('woonoow-customer-app');
|
||||
const initialRoute = appEl?.getAttribute('data-initial-route');
|
||||
return initialRoute || '/shop'; // Default to shop if not specified
|
||||
if (initialRoute) return initialRoute;
|
||||
|
||||
// Derive from SPA mode if no explicit route
|
||||
const spaMode = (window as any).woonoowCustomer?.spaMode || 'full';
|
||||
if (spaMode === 'checkout_only') return '/checkout';
|
||||
return '/shop'; // Default for full mode
|
||||
};
|
||||
|
||||
// Get front page slug from config
|
||||
const getFrontPageSlug = () => {
|
||||
return (window as any).woonoowCustomer?.frontPageSlug || null;
|
||||
};
|
||||
|
||||
// Router wrapper component that uses hooks requiring Router context
|
||||
function AppRoutes() {
|
||||
const initialRoute = getInitialRoute();
|
||||
const frontPageSlug = getFrontPageSlug();
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Routes>
|
||||
{/* Root route redirects to initial route based on SPA mode */}
|
||||
<Route path="/" element={<Navigate to={initialRoute} replace />} />
|
||||
{/* Root route: If frontPageSlug exists, render it. Else redirect to initialRoute (e.g. /shop) */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
frontPageSlug ? (
|
||||
<DynamicPageRenderer slug={frontPageSlug} />
|
||||
) : (
|
||||
<Navigate to={initialRoute === '/' ? '/shop' : initialRoute} replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Shop Routes */}
|
||||
<Route path="/shop" element={<Shop />} />
|
||||
@@ -128,6 +148,24 @@ function App() {
|
||||
const appearanceSettings = getAppearanceSettings();
|
||||
const toastPosition = (appearanceSettings?.general?.toast_position || 'top-right') as any;
|
||||
|
||||
// Inject gradient CSS variables
|
||||
React.useEffect(() => {
|
||||
// appearanceSettings is already the 'data' object from Assets.php injection
|
||||
// Structure: { general: { colors: { primary, secondary, accent, text, background, gradientStart, gradientEnd } } }
|
||||
const colors = appearanceSettings?.general?.colors;
|
||||
if (colors) {
|
||||
const root = document.documentElement;
|
||||
// Inject all color settings as CSS variables
|
||||
if (colors.primary) root.style.setProperty('--wn-primary', colors.primary);
|
||||
if (colors.secondary) root.style.setProperty('--wn-secondary', colors.secondary);
|
||||
if (colors.accent) root.style.setProperty('--wn-accent', colors.accent);
|
||||
if (colors.text) root.style.setProperty('--wn-text', colors.text);
|
||||
if (colors.background) root.style.setProperty('--wn-background', colors.background);
|
||||
if (colors.gradientStart) root.style.setProperty('--wn-gradient-start', colors.gradientStart);
|
||||
if (colors.gradientEnd) root.style.setProperty('--wn-gradient-end', colors.gradientEnd);
|
||||
}
|
||||
}, [appearanceSettings]);
|
||||
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
155
customer-spa/src/components/SharedContentLayout.tsx
Normal file
155
customer-spa/src/components/SharedContentLayout.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
|
||||
interface SharedContentProps {
|
||||
// Content
|
||||
title?: string;
|
||||
text?: string; // HTML content
|
||||
|
||||
// Image
|
||||
image?: string;
|
||||
imagePosition?: 'left' | 'right' | 'top' | 'bottom';
|
||||
|
||||
// Layout
|
||||
containerWidth?: 'full' | 'contained';
|
||||
|
||||
// Styles
|
||||
className?: string;
|
||||
titleStyle?: React.CSSProperties;
|
||||
titleClassName?: string;
|
||||
textStyle?: React.CSSProperties;
|
||||
textClassName?: string;
|
||||
headingStyle?: React.CSSProperties; // For prose headings override
|
||||
imageStyle?: React.CSSProperties;
|
||||
|
||||
// Pro Features (for future)
|
||||
buttons?: Array<{ text: string, url: string }>;
|
||||
buttonStyle?: { classNames?: string; style?: React.CSSProperties };
|
||||
}
|
||||
|
||||
|
||||
export const SharedContentLayout: React.FC<SharedContentProps> = ({
|
||||
title,
|
||||
text,
|
||||
image,
|
||||
imagePosition = 'left',
|
||||
containerWidth = 'contained',
|
||||
className,
|
||||
titleStyle,
|
||||
titleClassName,
|
||||
textStyle,
|
||||
textClassName,
|
||||
headingStyle,
|
||||
buttons,
|
||||
|
||||
imageStyle,
|
||||
buttonStyle
|
||||
}) => {
|
||||
|
||||
const hasImage = !!image;
|
||||
const isImageLeft = imagePosition === 'left';
|
||||
const isImageRight = imagePosition === 'right';
|
||||
const isImageTop = imagePosition === 'top';
|
||||
const isImageBottom = imagePosition === 'bottom';
|
||||
|
||||
// Wrapper classes
|
||||
const containerClasses = cn(
|
||||
'w-full mx-auto px-4 sm:px-6 lg:px-8',
|
||||
containerWidth === 'contained' ? 'max-w-7xl' : ''
|
||||
);
|
||||
|
||||
const gridClasses = cn(
|
||||
'mx-auto',
|
||||
hasImage && (isImageLeft || isImageRight) ? 'grid grid-cols-1 lg:grid-cols-2 gap-12 items-center' : 'max-w-4xl'
|
||||
);
|
||||
|
||||
const imageWrapperOrder = isImageRight ? 'lg:order-last' : 'lg:order-first';
|
||||
|
||||
const proseStyle = {
|
||||
...textStyle,
|
||||
'--tw-prose-headings': headingStyle?.color,
|
||||
'--tw-prose-body': textStyle?.color,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={gridClasses}>
|
||||
{/* Image Side */}
|
||||
{hasImage && (
|
||||
<div className={cn(
|
||||
'relative w-full aspect-[4/3] rounded-2xl overflow-hidden shadow-lg',
|
||||
imageWrapperOrder,
|
||||
(isImageTop || isImageBottom) && 'mb-8' // spacing if stacked
|
||||
)} style={imageStyle}>
|
||||
<img
|
||||
src={image}
|
||||
alt={title || 'Section Image'}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Side */}
|
||||
<div className={cn('flex flex-col', hasImage ? 'bg-transparent' : '')}>
|
||||
{title && (
|
||||
<h2
|
||||
className={cn(
|
||||
"tracking-tight text-current mb-6",
|
||||
!titleClassName && "text-3xl font-bold sm:text-4xl",
|
||||
titleClassName
|
||||
)}
|
||||
style={titleStyle}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{text && (
|
||||
<div
|
||||
className={cn(
|
||||
'prose prose-lg max-w-none',
|
||||
'prose-h1:text-3xl prose-h1:font-bold prose-h1:mt-4 prose-h1:mb-2',
|
||||
'prose-h2:text-2xl prose-h2:font-bold prose-h2:mt-3 prose-h2:mb-2',
|
||||
'prose-headings:text-[var(--tw-prose-headings)]',
|
||||
'prose-p:text-[var(--tw-prose-body)]',
|
||||
'text-[var(--tw-prose-body)]',
|
||||
className,
|
||||
textClassName
|
||||
)}
|
||||
style={proseStyle}
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* Buttons */}
|
||||
{buttons && buttons.length > 0 && (
|
||||
<div className="mt-8 flex flex-wrap gap-4">
|
||||
{buttons.map((btn, idx) => (
|
||||
btn.text && btn.url && (
|
||||
<a
|
||||
key={idx}
|
||||
href={btn.url}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2",
|
||||
!buttonStyle?.style?.backgroundColor && "bg-primary",
|
||||
!buttonStyle?.style?.color && "text-primary-foreground hover:bg-primary/90",
|
||||
buttonStyle?.classNames
|
||||
)}
|
||||
style={buttonStyle?.style}
|
||||
>
|
||||
{btn.text}
|
||||
</a>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -68,7 +68,7 @@ export function SearchableSelect({
|
||||
type="button"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between border !rounded-lg px-4 py-2 text-left bg-white hover:bg-gray-50 focus:outline-none focus:border-gray-400",
|
||||
"w-full flex items-center justify-between border rounded-lg px-4 py-2 text-left bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary/20",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -37,25 +37,35 @@ interface AppearanceSettings {
|
||||
thankyou: any;
|
||||
account: any;
|
||||
};
|
||||
menus: {
|
||||
primary: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
type: 'page' | 'custom';
|
||||
value: string;
|
||||
target: '_self' | '_blank';
|
||||
}>;
|
||||
mobile: Array<any>;
|
||||
};
|
||||
}
|
||||
|
||||
export function useAppearanceSettings() {
|
||||
const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1';
|
||||
|
||||
|
||||
// Get preloaded settings from window object
|
||||
const preloadedSettings = (window as any).woonoowCustomer?.appearanceSettings;
|
||||
|
||||
|
||||
return useQuery<AppearanceSettings>({
|
||||
queryKey: ['appearance-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${apiRoot}/appearance/settings`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch appearance settings');
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
return data.data;
|
||||
},
|
||||
@@ -68,7 +78,7 @@ export function useAppearanceSettings() {
|
||||
|
||||
export function useShopSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
layout: {
|
||||
grid_columns: '3' as string,
|
||||
@@ -93,7 +103,7 @@ export function useShopSettings() {
|
||||
show_icon: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
layout: { ...defaultSettings.layout, ...(data?.pages?.shop?.layout || {}) },
|
||||
elements: { ...defaultSettings.elements, ...(data?.pages?.shop?.elements || {}) },
|
||||
@@ -105,7 +115,7 @@ export function useShopSettings() {
|
||||
|
||||
export function useProductSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
layout: {
|
||||
image_position: 'left' as string,
|
||||
@@ -127,7 +137,7 @@ export function useProductSettings() {
|
||||
hide_if_empty: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
layout: { ...defaultSettings.layout, ...(data?.pages?.product?.layout || {}) },
|
||||
elements: { ...defaultSettings.elements, ...(data?.pages?.product?.elements || {}) },
|
||||
@@ -139,7 +149,7 @@ export function useProductSettings() {
|
||||
|
||||
export function useCartSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
layout: {
|
||||
style: 'fullwidth' as string,
|
||||
@@ -152,7 +162,7 @@ export function useCartSettings() {
|
||||
shipping_calculator: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
layout: { ...defaultSettings.layout, ...(data?.pages?.cart?.layout || {}) },
|
||||
elements: { ...defaultSettings.elements, ...(data?.pages?.cart?.elements || {}) },
|
||||
@@ -162,7 +172,7 @@ export function useCartSettings() {
|
||||
|
||||
export function useCheckoutSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
layout: {
|
||||
style: 'two-column' as string,
|
||||
@@ -178,7 +188,7 @@ export function useCheckoutSettings() {
|
||||
payment_icons: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
layout: { ...defaultSettings.layout, ...(data?.pages?.checkout?.layout || {}) },
|
||||
elements: { ...defaultSettings.elements, ...(data?.pages?.checkout?.elements || {}) },
|
||||
@@ -188,7 +198,7 @@ export function useCheckoutSettings() {
|
||||
|
||||
export function useThankYouSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
template: 'basic',
|
||||
header_visibility: 'show',
|
||||
@@ -201,7 +211,7 @@ export function useThankYouSettings() {
|
||||
related_products: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
template: data?.pages?.thankyou?.template || defaultSettings.template,
|
||||
headerVisibility: data?.pages?.thankyou?.header_visibility || defaultSettings.header_visibility,
|
||||
@@ -215,7 +225,7 @@ export function useThankYouSettings() {
|
||||
|
||||
export function useAccountSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
layout: {
|
||||
navigation_style: 'sidebar' as string,
|
||||
@@ -228,7 +238,7 @@ export function useAccountSettings() {
|
||||
account_details: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
layout: { ...defaultSettings.layout, ...(data?.pages?.account?.layout || {}) },
|
||||
elements: { ...defaultSettings.elements, ...(data?.pages?.account?.elements || {}) },
|
||||
@@ -238,7 +248,7 @@ export function useAccountSettings() {
|
||||
|
||||
export function useHeaderSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
|
||||
return {
|
||||
style: data?.header?.style ?? 'classic',
|
||||
sticky: data?.header?.sticky ?? true,
|
||||
@@ -261,7 +271,7 @@ export function useHeaderSettings() {
|
||||
|
||||
export function useFooterSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
|
||||
return {
|
||||
columns: data?.footer?.columns ?? '4',
|
||||
style: data?.footer?.style ?? 'detailed',
|
||||
@@ -293,4 +303,15 @@ export function useFooterSettings() {
|
||||
},
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export function useMenuSettings() {
|
||||
const { data, isLoading } = useAppearanceSettings();
|
||||
|
||||
return {
|
||||
primary: data?.menus?.primary ?? [],
|
||||
mobile: data?.menus?.mobile ?? [],
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import { Search, ShoppingCart, User, Menu, X, Heart } from 'lucide-react';
|
||||
import { useLayout } from '../contexts/ThemeContext';
|
||||
import { useCartStore } from '../lib/cart/store';
|
||||
import { useHeaderSettings, useFooterSettings } from '../hooks/useAppearanceSettings';
|
||||
import { useHeaderSettings, useFooterSettings, useMenuSettings } from '../hooks/useAppearanceSettings';
|
||||
import { SearchModal } from '../components/SearchModal';
|
||||
import { NewsletterForm } from '../components/NewsletterForm';
|
||||
import { LayoutWrapper } from './LayoutWrapper';
|
||||
@@ -51,6 +51,7 @@ function ClassicLayout({ children }: BaseLayoutProps) {
|
||||
const { isEnabled } = useModules();
|
||||
const { settings: wishlistSettings } = useModuleSettings('wishlist');
|
||||
const footerSettings = useFooterSettings();
|
||||
const { primary: primaryMenu, mobile: mobileMenu } = useMenuSettings();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
|
||||
@@ -74,7 +75,7 @@ function ClassicLayout({ children }: BaseLayoutProps) {
|
||||
{/* Logo */}
|
||||
{headerSettings.elements.logo && (
|
||||
<div className={`flex-shrink-0 ${headerSettings.mobile_logo === 'center' ? 'max-md:mx-auto' : ''}`}>
|
||||
<Link to="/shop" className="flex items-center gap-3 group">
|
||||
<Link to="/" className="flex items-center gap-3 group">
|
||||
{storeLogo ? (
|
||||
<img
|
||||
src={storeLogo}
|
||||
@@ -103,9 +104,24 @@ function ClassicLayout({ children }: BaseLayoutProps) {
|
||||
{/* Navigation */}
|
||||
{headerSettings.elements.navigation && (
|
||||
<nav className="hidden md:flex items-center space-x-8">
|
||||
<Link to="/shop" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Shop</Link>
|
||||
<a href="/about" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">About</a>
|
||||
<a href="/contact" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Contact</a>
|
||||
{primaryMenu.length > 0 ? (
|
||||
primaryMenu.map(item => (
|
||||
item.type === 'page' ? (
|
||||
<Link key={item.id} to={`/${item.value.replace(/^\/+/, '')}`} target={item.target} className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">{item.label}</Link>
|
||||
) : (
|
||||
<a key={item.id} href={item.value} target={item.target} className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">{item.label}</a>
|
||||
)
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{(window as any).woonoowCustomer?.frontPageSlug && (
|
||||
<Link to="/" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Home</Link>
|
||||
)}
|
||||
<Link to="/shop" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Shop</Link>
|
||||
<a href="/about" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">About</a>
|
||||
<a href="/contact" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Contact</a>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
@@ -177,9 +193,13 @@ function ClassicLayout({ children }: BaseLayoutProps) {
|
||||
<div className="md:hidden border-t py-4">
|
||||
{headerSettings.elements.navigation && (
|
||||
<nav className="flex flex-col space-y-2 mb-4">
|
||||
<Link to="/shop" className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">Shop</Link>
|
||||
<a href="/about" className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">About</a>
|
||||
<a href="/contact" className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">Contact</a>
|
||||
{(mobileMenu.length > 0 ? mobileMenu : primaryMenu).map(item => (
|
||||
item.type === 'page' ? (
|
||||
<Link key={item.id} to={`/${item.value.replace(/^\/+/, '')}`} target={item.target} className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">{item.label}</Link>
|
||||
) : (
|
||||
<a key={item.id} href={item.value} target={item.target} className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">{item.label}</a>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
@@ -198,9 +218,13 @@ function ClassicLayout({ children }: BaseLayoutProps) {
|
||||
</div>
|
||||
{headerSettings.elements.navigation && (
|
||||
<nav className="flex flex-col p-4">
|
||||
<Link to="/shop" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">Shop</Link>
|
||||
<a href="/about" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">About</a>
|
||||
<a href="/contact" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">Contact</a>
|
||||
{(mobileMenu.length > 0 ? mobileMenu : primaryMenu).map(item => (
|
||||
item.type === 'page' ? (
|
||||
<Link key={item.id} to={`/${item.value.replace(/^\/+/, '')}`} onClick={() => setMobileMenuOpen(false)} target={item.target} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">{item.label}</Link>
|
||||
) : (
|
||||
<a key={item.id} href={item.value} onClick={() => setMobileMenuOpen(false)} target={item.target} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">{item.label}</a>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
@@ -367,6 +391,7 @@ function ModernLayout({ children }: BaseLayoutProps) {
|
||||
const headerSettings = useHeaderSettings();
|
||||
const { isEnabled } = useModules();
|
||||
const { settings: wishlistSettings } = useModuleSettings('wishlist');
|
||||
const { primary: primaryMenu, mobile: mobileMenu } = useMenuSettings();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
|
||||
@@ -381,7 +406,7 @@ function ModernLayout({ children }: BaseLayoutProps) {
|
||||
<div className={`flex flex-col items-center ${paddingClass}`}>
|
||||
{/* Logo - Centered */}
|
||||
{headerSettings.elements.logo && (
|
||||
<Link to="/shop" className="mb-4">
|
||||
<Link to="/" className="mb-4">
|
||||
{storeLogo ? (
|
||||
<img
|
||||
src={storeLogo}
|
||||
@@ -404,9 +429,24 @@ function ModernLayout({ children }: BaseLayoutProps) {
|
||||
<nav className="hidden md:flex items-center space-x-8">
|
||||
{headerSettings.elements.navigation && (
|
||||
<>
|
||||
<Link to="/shop" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Shop</Link>
|
||||
<a href="/about" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">About</a>
|
||||
<a href="/contact" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Contact</a>
|
||||
{primaryMenu.length > 0 ? (
|
||||
primaryMenu.map(item => (
|
||||
item.type === 'page' ? (
|
||||
<Link key={item.id} to={`/${item.value.replace(/^\/+/, '')}`} target={item.target} className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">{item.label}</Link>
|
||||
) : (
|
||||
<a key={item.id} href={item.value} target={item.target} className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">{item.label}</a>
|
||||
)
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{(window as any).woonoowCustomer?.frontPageSlug && (
|
||||
<Link to="/" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Home</Link>
|
||||
)}
|
||||
<Link to="/shop" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Shop</Link>
|
||||
<a href="/about" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">About</a>
|
||||
<a href="/contact" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors no-underline">Contact</a>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{headerSettings.elements.search && (
|
||||
@@ -456,9 +496,13 @@ function ModernLayout({ children }: BaseLayoutProps) {
|
||||
<div className="md:hidden border-t py-4">
|
||||
{headerSettings.elements.navigation && (
|
||||
<nav className="flex flex-col space-y-2 mb-4">
|
||||
<Link to="/shop" className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">Shop</Link>
|
||||
<a href="/about" className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">About</a>
|
||||
<a href="/contact" className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">Contact</a>
|
||||
{(mobileMenu.length > 0 ? mobileMenu : primaryMenu).map(item => (
|
||||
item.type === 'page' ? (
|
||||
<Link key={item.id} to={`/${item.value.replace(/^\/+/, '')}`} target={item.target} className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">{item.label}</Link>
|
||||
) : (
|
||||
<a key={item.id} href={item.value} target={item.target} className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">{item.label}</a>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
@@ -503,6 +547,7 @@ function BoutiqueLayout({ children }: BaseLayoutProps) {
|
||||
const headerSettings = useHeaderSettings();
|
||||
const { isEnabled } = useModules();
|
||||
const { settings: wishlistSettings } = useModuleSettings('wishlist');
|
||||
const { primary: primaryMenu, mobile: mobileMenu } = useMenuSettings();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
|
||||
@@ -520,7 +565,7 @@ function BoutiqueLayout({ children }: BaseLayoutProps) {
|
||||
|
||||
{headerSettings.elements.logo && (
|
||||
<div className="flex-shrink-0">
|
||||
<Link to="/shop">
|
||||
<Link to="/">
|
||||
{storeLogo ? (
|
||||
<img
|
||||
src={storeLogo}
|
||||
@@ -543,7 +588,24 @@ function BoutiqueLayout({ children }: BaseLayoutProps) {
|
||||
{(headerSettings.elements.navigation || hasActions) && (
|
||||
<nav className="hidden md:flex items-center space-x-8">
|
||||
{headerSettings.elements.navigation && (
|
||||
<Link to="/shop" className="text-sm uppercase tracking-wider text-gray-700 hover:text-gray-900 transition-colors no-underline">Shop</Link>
|
||||
<>
|
||||
{primaryMenu.length > 0 ? (
|
||||
primaryMenu.map(item => (
|
||||
item.type === 'page' ? (
|
||||
<Link key={item.id} to={`/${item.value.replace(/^\/+/, '')}`} target={item.target} className="text-sm uppercase tracking-wider text-gray-700 hover:text-gray-900 transition-colors no-underline">{item.label}</Link>
|
||||
) : (
|
||||
<a key={item.id} href={item.value} target={item.target} className="text-sm uppercase tracking-wider text-gray-700 hover:text-gray-900 transition-colors no-underline">{item.label}</a>
|
||||
)
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{(window as any).woonoowCustomer?.frontPageSlug && (
|
||||
<Link to="/" className="text-sm uppercase tracking-wider text-gray-700 hover:text-gray-900 transition-colors no-underline">Home</Link>
|
||||
)}
|
||||
<Link to="/shop" className="text-sm uppercase tracking-wider text-gray-700 hover:text-gray-900 transition-colors no-underline">Shop</Link>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{headerSettings.elements.search && (
|
||||
<button
|
||||
@@ -591,7 +653,13 @@ function BoutiqueLayout({ children }: BaseLayoutProps) {
|
||||
<div className="md:hidden border-t py-4">
|
||||
{headerSettings.elements.navigation && (
|
||||
<nav className="flex flex-col space-y-2 mb-4">
|
||||
<Link to="/shop" className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">Shop</Link>
|
||||
{(mobileMenu.length > 0 ? mobileMenu : primaryMenu).map(item => (
|
||||
item.type === 'page' ? (
|
||||
<Link key={item.id} to={`/${item.value.replace(/^\/+/, '')}`} target={item.target} className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">{item.label}</Link>
|
||||
) : (
|
||||
<a key={item.id} href={item.value} target={item.target} className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 no-underline">{item.label}</a>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
@@ -656,7 +724,7 @@ function LaunchLayout({ children }: BaseLayoutProps) {
|
||||
<div className="container mx-auto px-4">
|
||||
<div className={`flex items-center justify-center ${heightClass}`}>
|
||||
{headerSettings.elements.logo && (
|
||||
<Link to="/shop">
|
||||
<Link to="/">
|
||||
{storeLogo ? (
|
||||
<img
|
||||
src={storeLogo}
|
||||
|
||||
@@ -19,11 +19,29 @@ interface SectionProp {
|
||||
source?: string;
|
||||
}
|
||||
|
||||
interface SectionStyles {
|
||||
backgroundColor?: string;
|
||||
backgroundImage?: string;
|
||||
backgroundOverlay?: number;
|
||||
paddingTop?: string;
|
||||
paddingBottom?: string;
|
||||
contentWidth?: 'full' | 'contained';
|
||||
}
|
||||
|
||||
interface ElementStyle {
|
||||
color?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: string;
|
||||
textAlign?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
type: string;
|
||||
layoutVariant?: string;
|
||||
colorScheme?: string;
|
||||
styles?: SectionStyles;
|
||||
elementStyles?: Record<string, ElementStyle>;
|
||||
props: Record<string, SectionProp | any>;
|
||||
}
|
||||
|
||||
@@ -64,32 +82,64 @@ const SECTION_COMPONENTS: Record<string, React.ComponentType<any>> = {
|
||||
'contact_form': ContactFormSection,
|
||||
};
|
||||
|
||||
/**
|
||||
* Flatten section props by extracting .value from {type, value} objects
|
||||
* This transforms { title: { type: 'static', value: 'Hello' } }
|
||||
* into { title: 'Hello' }
|
||||
*/
|
||||
function flattenSectionProps(props: Record<string, any>): Record<string, any> {
|
||||
const flattened: Record<string, any> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (value && typeof value === 'object' && 'type' in value && 'value' in value) {
|
||||
// This is a {type, value} prop structure
|
||||
flattened[key] = value.value;
|
||||
} else if (value && typeof value === 'object' && 'type' in value && 'source' in value) {
|
||||
// This is a dynamic prop - use source as placeholder for now
|
||||
flattened[key] = `[${value.source}]`;
|
||||
} else {
|
||||
// Regular value, pass through
|
||||
flattened[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* DynamicPageRenderer
|
||||
* Renders structural pages and CPT template content
|
||||
*/
|
||||
export function DynamicPageRenderer() {
|
||||
const { pathBase, slug } = useParams<{ pathBase?: string; slug?: string }>();
|
||||
interface DynamicPageRendererProps {
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export function DynamicPageRenderer({ slug: propSlug }: DynamicPageRendererProps) {
|
||||
const { pathBase, slug: paramSlug } = useParams<{ pathBase?: string; slug?: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
// Use prop slug if provided, otherwise use param slug
|
||||
const effectiveSlug = propSlug || paramSlug;
|
||||
|
||||
// Determine if this is a page or CPT content
|
||||
const isStructuralPage = !pathBase;
|
||||
// If propSlug is provided, it's treated as a structural page (pathBase is undefined)
|
||||
const isStructuralPage = !pathBase || !!propSlug;
|
||||
const contentType = pathBase === 'blog' ? 'post' : pathBase;
|
||||
const contentSlug = slug || '';
|
||||
const contentSlug = effectiveSlug || '';
|
||||
|
||||
// Fetch page/content data
|
||||
const { data: pageData, isLoading, error } = useQuery<PageData>({
|
||||
queryKey: ['dynamic-page', pathBase, slug],
|
||||
queryKey: ['dynamic-page', pathBase, effectiveSlug],
|
||||
queryFn: async (): Promise<PageData> => {
|
||||
if (isStructuralPage) {
|
||||
// Fetch structural page
|
||||
const response = await api.get(`/pages/${slug}`);
|
||||
return response.data;
|
||||
// Fetch structural page - api.get returns JSON directly
|
||||
const response = await api.get<PageData>(`/pages/${contentSlug}`);
|
||||
return response;
|
||||
} else {
|
||||
// Fetch CPT content with template
|
||||
const response = await api.get(`/content/${contentType}/${contentSlug}`);
|
||||
return response.data;
|
||||
const response = await api.get<PageData>(`/content/${contentType}/${contentSlug}`);
|
||||
return response;
|
||||
}
|
||||
},
|
||||
retry: false,
|
||||
@@ -172,13 +222,42 @@ export function DynamicPageRenderer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionComponent
|
||||
<div
|
||||
key={section.id}
|
||||
id={section.id}
|
||||
layout={section.layoutVariant || 'default'}
|
||||
colorScheme={section.colorScheme || 'default'}
|
||||
{...section.props}
|
||||
/>
|
||||
className={`relative overflow-hidden ${!section.styles?.backgroundColor ? '' : ''}`}
|
||||
style={{
|
||||
backgroundColor: section.styles?.backgroundColor,
|
||||
paddingTop: section.styles?.paddingTop,
|
||||
paddingBottom: section.styles?.paddingBottom,
|
||||
}}
|
||||
>
|
||||
{/* Background Image & Overlay */}
|
||||
{section.styles?.backgroundImage && (
|
||||
<>
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: `url(${section.styles.backgroundImage})` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-black"
|
||||
style={{ opacity: (section.styles.backgroundOverlay || 0) / 100 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Content Wrapper */}
|
||||
<div className={`relative z-10 ${section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : 'w-full'}`}>
|
||||
<SectionComponent
|
||||
id={section.id}
|
||||
section={section} // Pass full section object for components that need raw data
|
||||
layout={section.layoutVariant || 'default'}
|
||||
colorScheme={section.colorScheme || 'default'}
|
||||
styles={section.styles}
|
||||
elementStyles={section.elementStyles}
|
||||
{...flattenSectionProps(section.props || {})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ interface CTABannerSectionProps {
|
||||
text?: string;
|
||||
button_text?: string;
|
||||
button_url?: string;
|
||||
elementStyles?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function CTABannerSection({
|
||||
@@ -18,7 +19,36 @@ export function CTABannerSection({
|
||||
text,
|
||||
button_text,
|
||||
button_url,
|
||||
}: CTABannerSectionProps) {
|
||||
elementStyles,
|
||||
styles,
|
||||
}: CTABannerSectionProps & { styles?: Record<string, any> }) {
|
||||
|
||||
// Helper to get text styles (including font family)
|
||||
const getTextStyles = (elementName: string) => {
|
||||
const styles = 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,
|
||||
borderColor: styles.borderColor,
|
||||
borderWidth: styles.borderWidth,
|
||||
borderRadius: styles.borderRadius,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const titleStyle = getTextStyles('title');
|
||||
const textStyle = getTextStyles('text');
|
||||
const btnStyle = getTextStyles('button_text');
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
@@ -26,7 +56,7 @@ export function CTABannerSection({
|
||||
'wn-section wn-cta-banner',
|
||||
`wn-cta-banner--${layout}`,
|
||||
`wn-scheme--${colorScheme}`,
|
||||
'py-16 md:py-20',
|
||||
'py-12 md:py-20',
|
||||
{
|
||||
'bg-primary text-primary-foreground': colorScheme === 'primary',
|
||||
'bg-secondary text-secondary-foreground': colorScheme === 'secondary',
|
||||
@@ -35,21 +65,36 @@ export function CTABannerSection({
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<div className={cn(
|
||||
"mx-auto px-4 text-center",
|
||||
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
||||
)}>
|
||||
{title && (
|
||||
<h2 className="wn-cta-banner__title text-3xl md:text-4xl font-bold mb-4">
|
||||
<h2
|
||||
className={cn(
|
||||
"wn-cta__title mb-6",
|
||||
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl",
|
||||
!elementStyles?.title?.fontWeight && "font-bold",
|
||||
titleStyle.classNames
|
||||
)}
|
||||
style={titleStyle.style}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{text && (
|
||||
<p className={cn(
|
||||
'wn-cta-banner__text text-lg md:text-xl mb-8 max-w-2xl mx-auto',
|
||||
'wn-cta-banner__text mb-8 max-w-2xl mx-auto',
|
||||
!elementStyles?.text?.fontSize && "text-lg md:text-xl",
|
||||
{
|
||||
'text-white/90': colorScheme === 'primary' || colorScheme === 'gradient',
|
||||
'text-gray-600': colorScheme === 'muted',
|
||||
}
|
||||
)}>
|
||||
},
|
||||
textStyle.classNames
|
||||
)}
|
||||
style={textStyle.style}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
)}
|
||||
@@ -58,12 +103,18 @@ export function CTABannerSection({
|
||||
<a
|
||||
href={button_url}
|
||||
className={cn(
|
||||
'wn-cta-banner__button inline-block px-8 py-3 rounded-lg font-semibold transition-all hover:transform hover:scale-105',
|
||||
{
|
||||
'bg-white text-primary': colorScheme === 'primary' || colorScheme === 'gradient',
|
||||
'bg-primary text-white': colorScheme === 'muted',
|
||||
}
|
||||
'wn-cta-banner__button inline-block px-8 py-3 rounded-lg font-semibold transition-all hover:opacity-90',
|
||||
!btnStyle.style?.backgroundColor && {
|
||||
'bg-white': colorScheme === 'primary' || colorScheme === 'gradient',
|
||||
'bg-primary': colorScheme === 'muted' || colorScheme === 'secondary',
|
||||
},
|
||||
!btnStyle.style?.color && {
|
||||
'text-primary': colorScheme === 'primary' || colorScheme === 'gradient',
|
||||
'text-white': colorScheme === 'muted' || colorScheme === 'secondary',
|
||||
},
|
||||
btnStyle.classNames
|
||||
)}
|
||||
style={btnStyle.style}
|
||||
>
|
||||
{button_text}
|
||||
</a>
|
||||
|
||||
@@ -9,6 +9,7 @@ interface ContactFormSectionProps {
|
||||
webhook_url?: string;
|
||||
redirect_url?: string;
|
||||
fields?: string[];
|
||||
elementStyles?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function ContactFormSection({
|
||||
@@ -19,8 +20,37 @@ export function ContactFormSection({
|
||||
webhook_url,
|
||||
redirect_url,
|
||||
fields = ['name', 'email', 'message'],
|
||||
}: ContactFormSectionProps) {
|
||||
elementStyles,
|
||||
styles,
|
||||
}: ContactFormSectionProps & { styles?: Record<string, any> }) {
|
||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||
|
||||
// Helper to get text styles (including font family)
|
||||
const getTextStyles = (elementName: string) => {
|
||||
const styles = 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,
|
||||
borderColor: styles.borderColor,
|
||||
borderWidth: styles.borderWidth,
|
||||
borderRadius: styles.borderRadius,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const titleStyle = getTextStyles('title');
|
||||
const buttonStyle = getTextStyles('button');
|
||||
const fieldsStyle = getTextStyles('fields');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -65,14 +95,18 @@ export function ContactFormSection({
|
||||
className={cn(
|
||||
'wn-section wn-contact-form',
|
||||
`wn-scheme--${colorScheme}`,
|
||||
'py-16 md:py-20',
|
||||
`wn-scheme--${colorScheme}`,
|
||||
'py-12 md:py-20',
|
||||
{
|
||||
'bg-white': colorScheme === 'default',
|
||||
// 'bg-white': colorScheme === 'default', // Removed for global styling
|
||||
'bg-muted': colorScheme === 'muted',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className={cn(
|
||||
"mx-auto px-4",
|
||||
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
||||
)}>
|
||||
<div className={cn(
|
||||
'max-w-xl mx-auto',
|
||||
{
|
||||
@@ -80,7 +114,14 @@ export function ContactFormSection({
|
||||
}
|
||||
)}>
|
||||
{title && (
|
||||
<h2 className="wn-contact-form__title text-3xl font-bold text-center mb-8">
|
||||
<h2 className={cn(
|
||||
"wn-contact__title text-center mb-12",
|
||||
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl",
|
||||
!elementStyles?.title?.fontWeight && "font-bold",
|
||||
titleStyle.classNames
|
||||
)}
|
||||
style={titleStyle.style}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
@@ -101,7 +142,16 @@ export function ContactFormSection({
|
||||
value={formData[field] || ''}
|
||||
onChange={handleChange}
|
||||
rows={5}
|
||||
className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
className={cn(
|
||||
"w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary",
|
||||
fieldsStyle.classNames
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: fieldsStyle.style?.backgroundColor,
|
||||
color: fieldsStyle.style?.color,
|
||||
borderColor: fieldsStyle.style?.borderColor,
|
||||
borderRadius: fieldsStyle.style?.borderRadius,
|
||||
}}
|
||||
placeholder={`Enter your ${fieldLabel.toLowerCase()}`}
|
||||
required
|
||||
/>
|
||||
@@ -111,7 +161,16 @@ export function ContactFormSection({
|
||||
name={field}
|
||||
value={formData[field] || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
className={cn(
|
||||
"w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary",
|
||||
fieldsStyle.classNames
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: fieldsStyle.style?.backgroundColor,
|
||||
color: fieldsStyle.style?.color,
|
||||
borderColor: fieldsStyle.style?.borderColor,
|
||||
borderRadius: fieldsStyle.style?.borderRadius,
|
||||
}}
|
||||
placeholder={`Enter your ${fieldLabel.toLowerCase()}`}
|
||||
required
|
||||
/>
|
||||
@@ -128,10 +187,14 @@ export function ContactFormSection({
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className={cn(
|
||||
'w-full py-3 px-6 bg-primary text-primary-foreground rounded-lg font-semibold',
|
||||
'hover:bg-primary/90 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
'w-full py-3 px-6 rounded-lg font-semibold',
|
||||
'hover:opacity-90 transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
!buttonStyle.style?.backgroundColor && 'bg-primary',
|
||||
!buttonStyle.style?.color && 'text-primary-foreground',
|
||||
buttonStyle.classNames
|
||||
)}
|
||||
style={buttonStyle.style}
|
||||
>
|
||||
{submitting ? 'Sending...' : 'Submit'}
|
||||
</button>
|
||||
|
||||
@@ -1,46 +1,259 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
||||
|
||||
interface ContentSectionProps {
|
||||
id: string;
|
||||
layout?: string;
|
||||
colorScheme?: string;
|
||||
content?: string;
|
||||
section: {
|
||||
id: string;
|
||||
layoutVariant?: string;
|
||||
colorScheme?: string;
|
||||
props?: {
|
||||
content?: { value: string };
|
||||
cta_text?: { value: string };
|
||||
cta_url?: { value: string };
|
||||
};
|
||||
elementStyles?: Record<string, any>;
|
||||
styles?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
export function ContentSection({
|
||||
id,
|
||||
layout = 'default',
|
||||
colorScheme = 'default',
|
||||
content,
|
||||
}: ContentSectionProps) {
|
||||
if (!content) return null;
|
||||
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-screen-xl mx-auto',
|
||||
contained: 'max-w-screen-md mx-auto',
|
||||
full: 'w-full',
|
||||
};
|
||||
|
||||
const fontSizeToCSS = (className?: string) => {
|
||||
switch (className) {
|
||||
case 'text-xs': return '0.75rem';
|
||||
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';
|
||||
case 'text-7xl': return '4.5rem';
|
||||
case 'text-8xl': return '6rem';
|
||||
case 'text-9xl': return '8rem';
|
||||
default: return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const fontWeightToCSS = (className?: string) => {
|
||||
switch (className) {
|
||||
case 'font-thin': return '100';
|
||||
case 'font-extralight': return '200';
|
||||
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';
|
||||
case 'font-black': return '900';
|
||||
default: return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to generate scoped CSS for prose elements
|
||||
const generateScopedStyles = (sectionId: string, elementStyles: Record<string, any>) => {
|
||||
const styles: string[] = [];
|
||||
const scope = `#${sectionId}`; // ContentSection uses id directly on section tag
|
||||
|
||||
// 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;`,
|
||||
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
|
||||
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(' ');
|
||||
|
||||
styles.push(`${scope} a[data-button], ${scope} .button { ${btnRules} display: inline-block; text-decoration: none !important; }`);
|
||||
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 ContentSection({ section }: ContentSectionProps) {
|
||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
||||
// Default to 'default' width if not specified
|
||||
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 || '';
|
||||
|
||||
// Helper to get text styles
|
||||
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 textStyle = getTextStyles('text');
|
||||
const buttonStyle = getTextStyles('button');
|
||||
|
||||
const containerWidth = section.styles?.contentWidth || 'contained';
|
||||
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 (
|
||||
<section
|
||||
id={id}
|
||||
className={cn(
|
||||
'wn-section wn-content',
|
||||
`wn-scheme--${colorScheme}`,
|
||||
'py-12 md:py-16',
|
||||
{
|
||||
'bg-white': colorScheme === 'default',
|
||||
'bg-muted': colorScheme === 'muted',
|
||||
'bg-primary/5': colorScheme === 'primary',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="container mx-auto px-4">
|
||||
<div
|
||||
className={cn(
|
||||
'prose prose-lg max-w-none',
|
||||
{
|
||||
'max-w-3xl mx-auto': layout === 'narrow',
|
||||
'max-w-4xl mx-auto': layout === 'medium',
|
||||
}
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: generateScopedStyles(section.id, section.elementStyles || {}) }} />
|
||||
<section
|
||||
id={section.id}
|
||||
className={cn(
|
||||
'wn-content',
|
||||
'px-4 md:px-8',
|
||||
heightClasses,
|
||||
!scheme.bg.startsWith('wn-') && scheme.bg,
|
||||
scheme.text
|
||||
)}
|
||||
style={getBackgroundStyle()}
|
||||
>
|
||||
<SharedContentLayout
|
||||
text={content}
|
||||
textStyle={textStyle.style}
|
||||
headingStyle={headingStyle.style}
|
||||
containerWidth={containerWidth as any}
|
||||
className={contentStyle.classNames}
|
||||
buttons={cta_text && cta_url ? [{ text: cta_text, url: cta_url }] : []}
|
||||
buttonStyle={{
|
||||
classNames: buttonStyle.classNames,
|
||||
style: buttonStyle.style
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
|
||||
interface FeatureItem {
|
||||
title?: string;
|
||||
@@ -12,6 +13,7 @@ interface FeatureGridSectionProps {
|
||||
colorScheme?: string;
|
||||
heading?: string;
|
||||
items?: FeatureItem[];
|
||||
elementStyles?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function FeatureGridSection({
|
||||
@@ -20,13 +22,44 @@ export function FeatureGridSection({
|
||||
colorScheme = 'default',
|
||||
heading,
|
||||
items = [],
|
||||
}: FeatureGridSectionProps) {
|
||||
features = [],
|
||||
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 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)
|
||||
const getTextStyles = (elementName: string) => {
|
||||
const styles = 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,
|
||||
borderColor: styles.borderColor,
|
||||
borderWidth: styles.borderWidth,
|
||||
borderRadius: styles.borderRadius,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const headingStyle = getTextStyles('heading');
|
||||
const featureItemStyle = getTextStyles('feature_item');
|
||||
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
@@ -34,42 +67,65 @@ export function FeatureGridSection({
|
||||
'wn-section wn-feature-grid',
|
||||
`wn-feature-grid--${layout}`,
|
||||
`wn-scheme--${colorScheme}`,
|
||||
'py-16 md:py-24',
|
||||
'py-12 md:py-24',
|
||||
{
|
||||
'bg-white': colorScheme === 'default',
|
||||
// 'bg-white': colorScheme === 'default', // Removed for global styling
|
||||
'bg-muted': colorScheme === 'muted',
|
||||
'bg-primary text-primary-foreground': colorScheme === 'primary',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className={cn(
|
||||
"mx-auto px-4",
|
||||
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
||||
)}>
|
||||
{heading && (
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12">
|
||||
<h2
|
||||
className={cn(
|
||||
"wn-features__heading text-center mb-12",
|
||||
!elementStyles?.heading?.fontSize && "text-3xl md:text-4xl",
|
||||
!elementStyles?.heading?.fontWeight && "font-bold",
|
||||
headingStyle.classNames
|
||||
)}
|
||||
style={headingStyle.style}
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<div className={cn('grid gap-8', gridCols)}>
|
||||
{items.map((item, index) => (
|
||||
{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 && (
|
||||
<span className="wn-feature-grid__icon text-4xl mb-4 block">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
{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="wn-feature-grid__item-title text-xl font-semibold mb-3">
|
||||
<h3
|
||||
className={cn(
|
||||
"wn-feature-grid__item-title mb-3",
|
||||
!featureItemStyle.classNames && "text-xl font-semibold"
|
||||
)}
|
||||
style={{ color: featureItemStyle.style?.color }}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
)}
|
||||
@@ -77,11 +133,13 @@ export function FeatureGridSection({
|
||||
{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>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface HeroSectionProps {
|
||||
image?: string;
|
||||
cta_text?: string;
|
||||
cta_url?: string;
|
||||
elementStyles?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function HeroSection({
|
||||
@@ -20,11 +21,72 @@ export function HeroSection({
|
||||
image,
|
||||
cta_text,
|
||||
cta_url,
|
||||
}: HeroSectionProps) {
|
||||
elementStyles,
|
||||
styles,
|
||||
}: HeroSectionProps & { styles?: Record<string, any> }) {
|
||||
const isImageLeft = layout === 'hero-left-image' || layout === 'image-left';
|
||||
const isImageRight = layout === 'hero-right-image' || layout === 'image-right';
|
||||
const isCentered = layout === 'centered' || layout === 'default';
|
||||
|
||||
const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage;
|
||||
|
||||
// Helper to get text styles (including font family)
|
||||
const getTextStyles = (elementName: string) => {
|
||||
const styles = 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,
|
||||
borderColor: styles.borderColor,
|
||||
borderWidth: styles.borderWidth,
|
||||
borderRadius: styles.borderRadius,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const titleStyle = getTextStyles('title');
|
||||
const subtitleStyle = getTextStyles('subtitle');
|
||||
const ctaStyle = getTextStyles('cta_text');
|
||||
|
||||
const imageStyle = elementStyles?.['image'] || {};
|
||||
|
||||
// Determine height classes
|
||||
const heightPreset = styles?.heightPreset || 'default';
|
||||
const heightMap: Record<string, string> = {
|
||||
'default': 'py-12 md:py-24', // Original Default
|
||||
'small': 'py-8 md:py-16',
|
||||
'medium': 'py-16 md:py-32',
|
||||
'large': 'py-24 md:py-48',
|
||||
'screen': 'min-h-screen py-20 flex items-center',
|
||||
};
|
||||
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-24';
|
||||
|
||||
// Helper to get background style for dynamic schemes
|
||||
const getBackgroundStyle = () => {
|
||||
if (hasCustomBackground) return undefined;
|
||||
if (colorScheme === 'gradient') {
|
||||
return { backgroundImage: 'linear-gradient(135deg, var(--wn-gradient-start, #9333ea), var(--wn-gradient-end, #3b82f6))' };
|
||||
}
|
||||
if (colorScheme === 'primary') {
|
||||
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
||||
}
|
||||
if (colorScheme === 'secondary') {
|
||||
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isDynamicScheme = ['primary', 'secondary', 'gradient'].includes(colorScheme) && !hasCustomBackground;
|
||||
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
@@ -33,16 +95,15 @@ export function HeroSection({
|
||||
`wn-hero--${layout}`,
|
||||
`wn-scheme--${colorScheme}`,
|
||||
'relative overflow-hidden',
|
||||
{
|
||||
'bg-primary text-primary-foreground': colorScheme === 'primary',
|
||||
'bg-secondary text-secondary-foreground': colorScheme === 'secondary',
|
||||
'bg-muted': colorScheme === 'muted',
|
||||
'bg-gradient-to-r from-primary/10 to-secondary/10': colorScheme === 'gradient',
|
||||
}
|
||||
isDynamicScheme && 'text-white',
|
||||
colorScheme === 'muted' && !hasCustomBackground && 'bg-muted',
|
||||
)}
|
||||
style={getBackgroundStyle()}
|
||||
>
|
||||
<div className={cn(
|
||||
'container mx-auto px-4 py-16 md:py-24',
|
||||
'mx-auto px-4',
|
||||
heightClasses,
|
||||
styles?.contentWidth === 'full' ? 'w-full' : 'container',
|
||||
{
|
||||
'flex flex-col md:flex-row items-center gap-8': isImageLeft || isImageRight,
|
||||
'text-center': isCentered,
|
||||
@@ -51,11 +112,24 @@ export function HeroSection({
|
||||
{/* Image - Left */}
|
||||
{image && isImageLeft && (
|
||||
<div className="w-full md:w-1/2">
|
||||
<img
|
||||
src={image}
|
||||
alt={title || 'Hero image'}
|
||||
className="w-full h-auto rounded-lg shadow-xl"
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg shadow-xl overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: imageStyle.backgroundColor,
|
||||
width: imageStyle.width || 'auto',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={title || 'Hero image'}
|
||||
className="w-full h-auto block"
|
||||
style={{
|
||||
objectFit: imageStyle.objectFit,
|
||||
height: imageStyle.height,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -68,21 +142,68 @@ export function HeroSection({
|
||||
}
|
||||
)}>
|
||||
{title && (
|
||||
<h1 className="wn-hero__title text-4xl md:text-5xl lg:text-6xl font-bold leading-tight mb-6">
|
||||
<h1
|
||||
className={cn(
|
||||
"wn-hero__title mb-6 leading-tight",
|
||||
!elementStyles?.title?.fontSize && "text-4xl md:text-5xl lg:text-6xl",
|
||||
!elementStyles?.title?.fontWeight && "font-bold",
|
||||
titleStyle.classNames
|
||||
)}
|
||||
style={titleStyle.style}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
{subtitle && (
|
||||
<p className="wn-hero__subtitle text-lg md:text-xl text-opacity-80 mb-8">
|
||||
<p
|
||||
className={cn(
|
||||
"wn-hero__subtitle text-opacity-80 mb-8",
|
||||
!elementStyles?.subtitle?.fontSize && "text-lg md:text-xl",
|
||||
subtitleStyle.classNames
|
||||
)}
|
||||
style={subtitleStyle.style}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Centered Image */}
|
||||
<div
|
||||
className={cn(
|
||||
"mt-12 mx-auto rounded-lg shadow-xl overflow-hidden",
|
||||
imageStyle.width ? "" : "max-w-4xl"
|
||||
)}
|
||||
style={{ backgroundColor: imageStyle.backgroundColor, width: imageStyle.width || 'auto', maxWidth: '100%' }}
|
||||
>
|
||||
{image && isCentered && (
|
||||
<img
|
||||
src={image}
|
||||
alt={title || 'Hero image'}
|
||||
className={cn(
|
||||
"w-full rounded-[inherit]",
|
||||
!imageStyle.height && "h-auto",
|
||||
!imageStyle.objectFit && "object-cover"
|
||||
)}
|
||||
style={{
|
||||
objectFit: imageStyle.objectFit,
|
||||
height: imageStyle.height,
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{cta_text && cta_url && (
|
||||
<a
|
||||
href={cta_url}
|
||||
className="wn-hero__cta inline-block px-8 py-3 bg-primary text-primary-foreground rounded-lg font-semibold hover:bg-primary/90 transition-colors"
|
||||
className={cn(
|
||||
"wn-hero__cta inline-block px-8 py-3 rounded-lg font-semibold hover:opacity-90 transition-colors mt-8",
|
||||
!ctaStyle.style?.backgroundColor && "bg-primary",
|
||||
!ctaStyle.style?.color && "text-primary-foreground",
|
||||
ctaStyle.classNames
|
||||
)}
|
||||
style={ctaStyle.style}
|
||||
>
|
||||
{cta_text}
|
||||
</a>
|
||||
@@ -92,22 +213,24 @@ export function HeroSection({
|
||||
{/* Image - Right */}
|
||||
{image && isImageRight && (
|
||||
<div className="w-full md:w-1/2">
|
||||
<img
|
||||
src={image}
|
||||
alt={title || 'Hero image'}
|
||||
className="w-full h-auto rounded-lg shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Centered Image */}
|
||||
{image && isCentered && (
|
||||
<div className="mt-12">
|
||||
<img
|
||||
src={image}
|
||||
alt={title || 'Hero image'}
|
||||
className="w-full max-w-4xl mx-auto h-auto rounded-lg shadow-xl"
|
||||
/>
|
||||
<div
|
||||
className="rounded-lg shadow-xl overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: imageStyle.backgroundColor,
|
||||
width: imageStyle.width || 'auto',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={title || 'Hero image'}
|
||||
className="w-full h-auto block"
|
||||
style={{
|
||||
objectFit: imageStyle.objectFit,
|
||||
height: imageStyle.height,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
||||
|
||||
interface ImageTextSectionProps {
|
||||
id: string;
|
||||
@@ -7,6 +8,7 @@ interface ImageTextSectionProps {
|
||||
title?: string;
|
||||
text?: string;
|
||||
image?: string;
|
||||
elementStyles?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function ImageTextSection({
|
||||
@@ -16,60 +18,87 @@ export function ImageTextSection({
|
||||
title,
|
||||
text,
|
||||
image,
|
||||
}: ImageTextSectionProps) {
|
||||
cta_text,
|
||||
cta_url,
|
||||
elementStyles,
|
||||
styles,
|
||||
}: ImageTextSectionProps & { styles?: Record<string, any>, cta_text?: string, cta_url?: string }) {
|
||||
const isImageLeft = layout === 'image-left' || layout === 'left';
|
||||
const isImageRight = layout === 'image-right' || layout === 'right';
|
||||
|
||||
// Helper to get text styles (including font family)
|
||||
const getTextStyles = (elementName: string) => {
|
||||
const styles = 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,
|
||||
borderColor: styles.borderColor,
|
||||
borderWidth: styles.borderWidth,
|
||||
borderRadius: styles.borderRadius,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const titleStyle = getTextStyles('title');
|
||||
const textStyle = getTextStyles('text');
|
||||
const buttonStyle = getTextStyles('button');
|
||||
|
||||
const imageStyle = elementStyles?.['image'] || {};
|
||||
|
||||
// Height preset support
|
||||
const heightPreset = styles?.heightPreset || 'default';
|
||||
const heightMap: Record<string, string> = {
|
||||
'default': 'py-12 md:py-24',
|
||||
'small': 'py-8 md:py-16',
|
||||
'medium': 'py-16 md:py-32',
|
||||
'large': 'py-24 md:py-48',
|
||||
'screen': 'min-h-screen py-20 flex items-center',
|
||||
};
|
||||
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-24';
|
||||
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
className={cn(
|
||||
'wn-section wn-image-text',
|
||||
`wn-image-text--${layout}`,
|
||||
`wn-scheme--${colorScheme}`,
|
||||
'py-16 md:py-24',
|
||||
heightClasses,
|
||||
{
|
||||
'bg-white': colorScheme === 'default',
|
||||
'bg-muted': colorScheme === 'muted',
|
||||
'bg-primary/5': colorScheme === 'primary',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className={cn(
|
||||
'flex flex-col md:flex-row items-center gap-8 md:gap-16',
|
||||
{
|
||||
'md:flex-row-reverse': isImageRight,
|
||||
}
|
||||
)}>
|
||||
{/* Image */}
|
||||
{image && (
|
||||
<div className="w-full md:w-1/2">
|
||||
<img
|
||||
src={image}
|
||||
alt={title || 'Section image'}
|
||||
className="w-full h-auto rounded-xl shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="w-full md:w-1/2">
|
||||
{title && (
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{text && (
|
||||
<div
|
||||
className="prose prose-lg text-gray-600"
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SharedContentLayout
|
||||
title={title}
|
||||
text={text}
|
||||
image={image}
|
||||
imagePosition={isImageRight ? 'right' : 'left'}
|
||||
containerWidth={styles?.contentWidth === 'full' ? 'full' : 'contained'}
|
||||
titleStyle={titleStyle.style}
|
||||
titleClassName={titleStyle.classNames}
|
||||
textStyle={textStyle.style}
|
||||
textClassName={textStyle.classNames}
|
||||
imageStyle={{
|
||||
backgroundColor: imageStyle.backgroundColor,
|
||||
objectFit: imageStyle.objectFit,
|
||||
}}
|
||||
buttons={cta_text && cta_url ? [{ text: cta_text, url: cta_url }] : []}
|
||||
buttonStyle={{
|
||||
classNames: buttonStyle.classNames,
|
||||
style: buttonStyle.style
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ export default function Shop() {
|
||||
placeholder="Search products..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pl-10 pr-10 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full !pl-10 pr-10 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user