feat(pages): Page Editor module fixing and improvements
This commit is contained in:
26
admin-spa/src/components/SectionBackgroundRenderer.tsx
Normal file
26
admin-spa/src/components/SectionBackgroundRenderer.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { SectionStyleResult } from '@/lib/sectionStyles';
|
||||||
|
|
||||||
|
interface SectionBackgroundRendererProps {
|
||||||
|
bg: SectionStyleResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionBackgroundRenderer({ bg }: SectionBackgroundRendererProps) {
|
||||||
|
if (!bg.backgroundImage) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 pointer-events-none z-0">
|
||||||
|
<img
|
||||||
|
src={bg.backgroundImage}
|
||||||
|
alt=""
|
||||||
|
role="presentation"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
{bg.hasOverlay && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black"
|
||||||
|
style={{ opacity: bg.overlayOpacity }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -207,10 +207,10 @@ export function CanvasRenderer({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-white transition-all duration-300 min-h-[500px]',
|
'bg-white transition-all duration-300 min-h-[500px] wn-page',
|
||||||
deviceMode === 'mobile'
|
deviceMode === 'mobile'
|
||||||
? 'max-w-sm mx-auto shadow-2xl rounded-[2.5rem] border-[12px] border-gray-800 my-8 overflow-hidden'
|
? 'max-w-sm mx-auto shadow-2xl rounded-[2.5rem] border-[12px] border-gray-800 my-8 overflow-hidden'
|
||||||
: 'w-full h-full'
|
: cn('h-full', containerWidth === 'boxed' ? 'container mx-auto max-w-6xl shadow-sm border-x border-b' : 'w-full')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{sections.length === 0 ? (
|
{sections.length === 0 ? (
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ export function CanvasSection({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"relative z-10",
|
"relative z-10 w-full",
|
||||||
section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : 'w-full'
|
section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : ''
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Plus, Layout, Undo2, Save, Maximize2, Minimize2 } from 'lucide-react';
|
import { Plus, Layout, Undo2, Redo2, Save, Maximize2, Minimize2 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -50,6 +50,11 @@ export default function AppearancePages() {
|
|||||||
setInspectorCollapsed,
|
setInspectorCollapsed,
|
||||||
setAvailableSources,
|
setAvailableSources,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
past,
|
||||||
|
future,
|
||||||
|
updateCurrentPage,
|
||||||
addSection,
|
addSection,
|
||||||
deleteSection,
|
deleteSection,
|
||||||
duplicateSection,
|
duplicateSection,
|
||||||
@@ -61,6 +66,7 @@ export default function AppearancePages() {
|
|||||||
updateSectionStyles,
|
updateSectionStyles,
|
||||||
updateElementStyles,
|
updateElementStyles,
|
||||||
markAsSaved,
|
markAsSaved,
|
||||||
|
markAsChanged,
|
||||||
setAsSpaLanding,
|
setAsSpaLanding,
|
||||||
unsetSpaLanding,
|
unsetSpaLanding,
|
||||||
} = usePageEditorStore();
|
} = usePageEditorStore();
|
||||||
@@ -160,7 +166,10 @@ export default function AppearancePages() {
|
|||||||
const endpoint = currentPage.type === 'page'
|
const endpoint = currentPage.type === 'page'
|
||||||
? `/pages/${currentPage.slug}`
|
? `/pages/${currentPage.slug}`
|
||||||
: `/templates/${currentPage.cpt}`;
|
: `/templates/${currentPage.cpt}`;
|
||||||
return api.post(endpoint, { sections });
|
return api.post(endpoint, {
|
||||||
|
sections,
|
||||||
|
container_width: currentPage.containerWidth
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(__('Page saved successfully'));
|
toast.success(__('Page saved successfully'));
|
||||||
@@ -332,17 +341,40 @@ export default function AppearancePages() {
|
|||||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
{hasUnsavedChanges && (
|
{hasUnsavedChanges && (
|
||||||
<>
|
<span className="text-sm text-amber-600">{__('Unsaved changes')}</span>
|
||||||
<span className="text-sm text-amber-600">{__('Unsaved changes')}</span>
|
)}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
<div className="flex items-center rounded-md border bg-muted/50 p-0.5">
|
||||||
size="sm"
|
<Button
|
||||||
onClick={handleDiscard}
|
variant="ghost"
|
||||||
>
|
size="icon"
|
||||||
<Undo2 className="w-4 h-4 mr-2" />
|
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||||
{__('Discard')}
|
onClick={undo}
|
||||||
</Button>
|
disabled={past.length === 0}
|
||||||
</>
|
title={__('Undo')}
|
||||||
|
>
|
||||||
|
<Undo2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={redo}
|
||||||
|
disabled={future.length === 0}
|
||||||
|
title={__('Redo')}
|
||||||
|
>
|
||||||
|
<Redo2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasUnsavedChanges && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDiscard}
|
||||||
|
>
|
||||||
|
{__('Discard')}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -455,10 +487,7 @@ export default function AppearancePages() {
|
|||||||
onDeletePage={handleDeletePage}
|
onDeletePage={handleDeletePage}
|
||||||
onDeleteTemplate={handleDeleteTemplate}
|
onDeleteTemplate={handleDeleteTemplate}
|
||||||
onContainerWidthChange={(width) => {
|
onContainerWidthChange={(width) => {
|
||||||
if (currentPage) {
|
updateCurrentPage({ containerWidth: width });
|
||||||
setCurrentPage({ ...currentPage, containerWidth: width });
|
|
||||||
markAsSaved(); // Mark as changed so save button enables
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export const SECTION_SCHEMAS: Record<string, SectionSchema> = {
|
|||||||
stylableElements: [
|
stylableElements: [
|
||||||
{ name: 'heading', label: 'Heading', type: 'text' },
|
{ name: 'heading', label: 'Heading', type: 'text' },
|
||||||
{ name: 'feature_item', label: 'Feature Item (Card)', type: 'text' },
|
{ name: 'feature_item', label: 'Feature Item (Card)', type: 'text' },
|
||||||
|
{ name: 'link', label: 'Link (Read more)', type: 'text' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'cta-banner': {
|
'cta-banner': {
|
||||||
@@ -233,6 +234,7 @@ export const SECTION_SCHEMAS: Record<string, SectionSchema> = {
|
|||||||
stylableElements: [
|
stylableElements: [
|
||||||
{ name: 'title', label: 'Title', type: 'text' },
|
{ name: 'title', label: 'Title', type: 'text' },
|
||||||
{ name: 'subtitle', label: 'Subtitle', type: 'text' },
|
{ name: 'subtitle', label: 'Subtitle', type: 'text' },
|
||||||
|
{ name: 'link', label: 'CTA Link', type: 'text' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'shoppable-image': {
|
'shoppable-image': {
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ export interface PageItem {
|
|||||||
isSpaLanding?: boolean;
|
isSpaLanding?: boolean;
|
||||||
containerWidth?: 'boxed' | 'fullwidth' | 'default';
|
containerWidth?: 'boxed' | 'fullwidth' | 'default';
|
||||||
}
|
}
|
||||||
|
interface HistoryState {
|
||||||
|
sections: Section[];
|
||||||
|
currentPage: PageItem | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface PageEditorState {
|
interface PageEditorState {
|
||||||
// Current page/template being edited
|
// Current page/template being edited
|
||||||
@@ -91,6 +95,10 @@ interface PageEditorState {
|
|||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|
||||||
|
// History (Undo/Redo)
|
||||||
|
past: HistoryState[];
|
||||||
|
future: HistoryState[];
|
||||||
|
|
||||||
// Available sources for dynamic fields (CPT templates)
|
// Available sources for dynamic fields (CPT templates)
|
||||||
availableSources: { value: string; label: string }[];
|
availableSources: { value: string; label: string }[];
|
||||||
|
|
||||||
@@ -104,6 +112,14 @@ interface PageEditorState {
|
|||||||
setAvailableSources: (sources: { value: string; label: string }[]) => void;
|
setAvailableSources: (sources: { value: string; label: string }[]) => void;
|
||||||
setIsLoading: (loading: boolean) => void;
|
setIsLoading: (loading: boolean) => void;
|
||||||
|
|
||||||
|
// History actions
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
pushHistory: () => void;
|
||||||
|
|
||||||
|
// Page updates
|
||||||
|
updateCurrentPage: (updates: Partial<PageItem>) => void;
|
||||||
|
|
||||||
// Section actions
|
// Section actions
|
||||||
addSection: (type: string, index?: number) => void;
|
addSection: (type: string, index?: number) => void;
|
||||||
deleteSection: (id: string) => void;
|
deleteSection: (id: string) => void;
|
||||||
@@ -137,11 +153,13 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
inspectorCollapsed: false,
|
inspectorCollapsed: false,
|
||||||
hasUnsavedChanges: false,
|
hasUnsavedChanges: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
past: [],
|
||||||
|
future: [],
|
||||||
availableSources: [],
|
availableSources: [],
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
setCurrentPage: (currentPage) => set({ currentPage }),
|
setCurrentPage: (currentPage) => set({ currentPage }),
|
||||||
setSections: (sections) => set({ sections, hasUnsavedChanges: true }),
|
setSections: (sections) => set({ sections, hasUnsavedChanges: true, past: [], future: [] }),
|
||||||
setSelectedSection: (selectedSectionId) => set({ selectedSectionId }),
|
setSelectedSection: (selectedSectionId) => set({ selectedSectionId }),
|
||||||
setHoveredSection: (hoveredSectionId) => set({ hoveredSectionId }),
|
setHoveredSection: (hoveredSectionId) => set({ hoveredSectionId }),
|
||||||
setDeviceMode: (deviceMode) => set({ deviceMode }),
|
setDeviceMode: (deviceMode) => set({ deviceMode }),
|
||||||
@@ -149,9 +167,64 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
setAvailableSources: (availableSources) => set({ availableSources }),
|
setAvailableSources: (availableSources) => set({ availableSources }),
|
||||||
setIsLoading: (isLoading) => set({ isLoading }),
|
setIsLoading: (isLoading) => set({ isLoading }),
|
||||||
|
|
||||||
|
// History actions
|
||||||
|
pushHistory: () => {
|
||||||
|
const { sections, currentPage } = get();
|
||||||
|
set((state) => ({
|
||||||
|
past: [...state.past, {
|
||||||
|
sections: JSON.parse(JSON.stringify(sections)),
|
||||||
|
currentPage: JSON.parse(JSON.stringify(currentPage))
|
||||||
|
}],
|
||||||
|
future: []
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
undo: () => {
|
||||||
|
const { past, future, sections, currentPage } = get();
|
||||||
|
if (past.length === 0) return;
|
||||||
|
|
||||||
|
const previous = past[past.length - 1];
|
||||||
|
const newPast = past.slice(0, past.length - 1);
|
||||||
|
|
||||||
|
set({
|
||||||
|
past: newPast,
|
||||||
|
future: [{ sections, currentPage }, ...future],
|
||||||
|
sections: previous.sections,
|
||||||
|
currentPage: previous.currentPage,
|
||||||
|
hasUnsavedChanges: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
redo: () => {
|
||||||
|
const { past, future, sections, currentPage } = get();
|
||||||
|
if (future.length === 0) return;
|
||||||
|
|
||||||
|
const next = future[0];
|
||||||
|
const newFuture = future.slice(1);
|
||||||
|
|
||||||
|
set({
|
||||||
|
past: [...past, { sections, currentPage }],
|
||||||
|
future: newFuture,
|
||||||
|
sections: next.sections,
|
||||||
|
currentPage: next.currentPage,
|
||||||
|
hasUnsavedChanges: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Page updates
|
||||||
|
updateCurrentPage: (updates) => {
|
||||||
|
const { currentPage, pushHistory } = get();
|
||||||
|
if (!currentPage) return;
|
||||||
|
pushHistory();
|
||||||
|
set({
|
||||||
|
currentPage: { ...currentPage, ...updates },
|
||||||
|
hasUnsavedChanges: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Section actions
|
// Section actions
|
||||||
addSection: (type, index) => {
|
addSection: (type, index) => {
|
||||||
const { sections } = get();
|
const { sections, pushHistory } = get();
|
||||||
const sectionConfig = getSectionSchema(type);
|
const sectionConfig = getSectionSchema(type);
|
||||||
|
|
||||||
if (!sectionConfig) return;
|
if (!sectionConfig) return;
|
||||||
@@ -163,6 +236,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
styles: cloneDefaultStyles(type) as SectionStyles,
|
styles: cloneDefaultStyles(type) as SectionStyles,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pushHistory();
|
||||||
|
|
||||||
const newSections = [...sections];
|
const newSections = [...sections];
|
||||||
if (typeof index === 'number') {
|
if (typeof index === 'number') {
|
||||||
newSections.splice(index, 0, newSection);
|
newSections.splice(index, 0, newSection);
|
||||||
@@ -177,7 +252,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteSection: (id) => {
|
deleteSection: (id) => {
|
||||||
const { sections, selectedSectionId } = get();
|
const { sections, selectedSectionId, pushHistory } = get();
|
||||||
|
pushHistory();
|
||||||
const newSections = sections.filter(s => s.id !== id);
|
const newSections = sections.filter(s => s.id !== id);
|
||||||
|
|
||||||
set({
|
set({
|
||||||
@@ -188,10 +264,12 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
duplicateSection: (id) => {
|
duplicateSection: (id) => {
|
||||||
const { sections } = get();
|
const { sections, pushHistory } = get();
|
||||||
const index = sections.findIndex(s => s.id === id);
|
const index = sections.findIndex(s => s.id === id);
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
|
pushHistory();
|
||||||
|
|
||||||
const section = sections[index];
|
const section = sections[index];
|
||||||
const newSection: Section = {
|
const newSection: Section = {
|
||||||
...JSON.parse(JSON.stringify(section)), // Deep clone
|
...JSON.parse(JSON.stringify(section)), // Deep clone
|
||||||
@@ -205,27 +283,32 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
moveSection: (id, direction) => {
|
moveSection: (id, direction) => {
|
||||||
const { sections } = get();
|
const { sections, pushHistory } = get();
|
||||||
const index = sections.findIndex(s => s.id === id);
|
const index = sections.findIndex(s => s.id === id);
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
if (direction === 'up' && index > 0) {
|
if (direction === 'up' && index > 0) {
|
||||||
|
pushHistory();
|
||||||
const newSections = [...sections];
|
const newSections = [...sections];
|
||||||
[newSections[index], newSections[index - 1]] = [newSections[index - 1], newSections[index]];
|
[newSections[index], newSections[index - 1]] = [newSections[index - 1], newSections[index]];
|
||||||
set({ sections: newSections, hasUnsavedChanges: true });
|
set({ sections: newSections, hasUnsavedChanges: true });
|
||||||
} else if (direction === 'down' && index < sections.length - 1) {
|
} else if (direction === 'down' && index < sections.length - 1) {
|
||||||
|
pushHistory();
|
||||||
const newSections = [...sections];
|
const newSections = [...sections];
|
||||||
[newSections[index], newSections[index + 1]] = [newSections[index + 1], newSections[index]];
|
[newSections[index], newSections[index + 1]] = [newSections[index + 1], newSections[index]];
|
||||||
set({ sections: newSections, hasUnsavedChanges: true });
|
set({ sections: newSections, hasUnsavedChanges: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reorderSections: (sections) => {
|
reorderSections: (newSections) => {
|
||||||
set({ sections, hasUnsavedChanges: true });
|
const { sections, pushHistory } = get();
|
||||||
|
pushHistory();
|
||||||
|
set({ sections: newSections, hasUnsavedChanges: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSectionProp: (sectionId, propName, value) => {
|
updateSectionProp: (sectionId, propName, value) => {
|
||||||
const { sections } = get();
|
const { sections, pushHistory } = get();
|
||||||
|
pushHistory();
|
||||||
const newSections = sections.map(section => {
|
const newSections = sections.map(section => {
|
||||||
if (section.id !== sectionId) return section;
|
if (section.id !== sectionId) return section;
|
||||||
return {
|
return {
|
||||||
@@ -240,7 +323,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateSectionLayout: (sectionId, layoutVariant) => {
|
updateSectionLayout: (sectionId, layoutVariant) => {
|
||||||
const { sections } = get();
|
const { sections, pushHistory } = get();
|
||||||
|
pushHistory();
|
||||||
const newSections = sections.map(section => {
|
const newSections = sections.map(section => {
|
||||||
if (section.id !== sectionId) return section;
|
if (section.id !== sectionId) return section;
|
||||||
return {
|
return {
|
||||||
@@ -252,7 +336,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateSectionColorScheme: (sectionId, colorScheme) => {
|
updateSectionColorScheme: (sectionId, colorScheme) => {
|
||||||
const { sections } = get();
|
const { sections, pushHistory } = get();
|
||||||
|
pushHistory();
|
||||||
const newSections = sections.map(section => {
|
const newSections = sections.map(section => {
|
||||||
if (section.id !== sectionId) return section;
|
if (section.id !== sectionId) return section;
|
||||||
return {
|
return {
|
||||||
@@ -264,7 +349,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateSectionStyles: (sectionId, styles) => {
|
updateSectionStyles: (sectionId, styles) => {
|
||||||
const { sections } = get();
|
const { sections, pushHistory } = get();
|
||||||
|
pushHistory();
|
||||||
const newSections = sections.map(section => {
|
const newSections = sections.map(section => {
|
||||||
if (section.id !== sectionId) return section;
|
if (section.id !== sectionId) return section;
|
||||||
return {
|
return {
|
||||||
@@ -279,7 +365,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateElementStyles: (sectionId, fieldName, styles) => {
|
updateElementStyles: (sectionId, fieldName, styles) => {
|
||||||
const { sections } = get();
|
const { sections, pushHistory } = get();
|
||||||
|
pushHistory();
|
||||||
const newSections = sections.map(section => {
|
const newSections = sections.map(section => {
|
||||||
if (section.id !== sectionId) return section;
|
if (section.id !== sectionId) return section;
|
||||||
|
|
||||||
|
|||||||
26
customer-spa/src/components/SectionBackgroundRenderer.tsx
Normal file
26
customer-spa/src/components/SectionBackgroundRenderer.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { SectionStyleResult } from '@/lib/sectionStyles';
|
||||||
|
|
||||||
|
interface SectionBackgroundRendererProps {
|
||||||
|
bg: SectionStyleResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionBackgroundRenderer({ bg }: SectionBackgroundRendererProps) {
|
||||||
|
if (!bg.backgroundImage) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 pointer-events-none z-0">
|
||||||
|
<img
|
||||||
|
src={bg.backgroundImage}
|
||||||
|
alt=""
|
||||||
|
role="presentation"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
{bg.hasOverlay && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black"
|
||||||
|
style={{ opacity: bg.overlayOpacity }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -53,19 +53,17 @@ export const SharedContentLayout: React.FC<SharedContentProps> = ({
|
|||||||
const isImageTop = imagePosition === 'top';
|
const isImageTop = imagePosition === 'top';
|
||||||
const isImageBottom = imagePosition === 'bottom';
|
const isImageBottom = imagePosition === 'bottom';
|
||||||
|
|
||||||
// Wrapper classes — full = edge-to-edge, contained = narrow readable column, boxed = card at max-w-5xl
|
// Wrapper classes — no width constraints applied here, parent handles it
|
||||||
const containerClasses = cn(
|
const containerClasses = cn(
|
||||||
'w-full mx-auto px-4 sm:px-6 lg:px-8',
|
'w-full mx-auto px-4 sm:px-6 lg:px-8',
|
||||||
containerWidth === 'contained' ? 'max-w-4xl'
|
containerWidth === 'contained' ? 'max-w-4xl' : '' // only constraint needed is for contained narrow text
|
||||||
: containerWidth === 'boxed' ? 'max-w-5xl'
|
|
||||||
: '' // full = no max-width cap
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const gridClasses = cn(
|
const gridClasses = cn(
|
||||||
'mx-auto',
|
'mx-auto w-full',
|
||||||
hasImage && (isImageLeft || isImageRight)
|
hasImage && (isImageLeft || isImageRight)
|
||||||
? 'grid grid-cols-1 lg:grid-cols-2 gap-12 items-center'
|
? 'grid grid-cols-1 lg:grid-cols-2 gap-12 items-center'
|
||||||
: containerWidth === 'full' ? 'w-full' : '' // no extra constraint for contained — outer already limits it
|
: ''
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageWrapperOrder = isImageRight ? 'lg:order-last' : 'lg:order-first';
|
const imageWrapperOrder = isImageRight ? 'lg:order-last' : 'lg:order-first';
|
||||||
@@ -78,161 +76,79 @@ export const SharedContentLayout: React.FC<SharedContentProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
{containerWidth === 'boxed' ? (
|
<div className={gridClasses}>
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden px-6 md:px-10 py-10">
|
{/* Image Side */}
|
||||||
<div className={gridClasses}>
|
{hasImage && (
|
||||||
{/* Image Side */}
|
<div className={cn(
|
||||||
{hasImage && (
|
'relative w-full aspect-[4/3] rounded-2xl overflow-hidden shadow-lg',
|
||||||
<div className={cn(
|
imageWrapperOrder,
|
||||||
'relative w-full aspect-[4/3] rounded-2xl overflow-hidden shadow-lg',
|
(isImageTop || isImageBottom) && 'mb-8'
|
||||||
imageWrapperOrder,
|
)} style={imageStyle}>
|
||||||
(isImageTop || isImageBottom) && 'mb-8' // spacing if stacked
|
<img
|
||||||
)} style={imageStyle}>
|
src={image}
|
||||||
<img
|
alt={title || 'Section Image'}
|
||||||
src={image}
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
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 lg:text-5xl",
|
|
||||||
titleClassName
|
|
||||||
)}
|
|
||||||
style={titleStyle}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{text && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'prose prose-lg max-w-none',
|
|
||||||
'prose-h1:text-3xl md:prose-h1:text-4xl lg:prose-h1:text-5xl prose-h1:font-bold prose-h1:mt-6 prose-h1:mb-4',
|
|
||||||
'prose-h2:text-2xl md:prose-h2:text-3xl lg:prose-h2:text-4xl prose-h2:font-bold prose-h2:mt-5 prose-h2:mb-3',
|
|
||||||
'prose-h3:text-xl md:prose-h3:text-2xl lg:prose-h3:text-3xl prose-h3:font-bold prose-h3:mt-4 prose-h3: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>
|
||||||
</div>
|
)}
|
||||||
) : (
|
|
||||||
<div className={gridClasses}>
|
{/* Content Side */}
|
||||||
{/* Image Side */}
|
<div className={cn('flex flex-col', hasImage ? 'bg-transparent' : '')}>
|
||||||
{hasImage && (
|
{title && (
|
||||||
<div className={cn(
|
<h2
|
||||||
'relative w-full aspect-[4/3] rounded-2xl overflow-hidden shadow-lg',
|
className={cn(
|
||||||
imageWrapperOrder,
|
"tracking-tight text-current mb-6 w-full",
|
||||||
(isImageTop || isImageBottom) && 'mb-8'
|
!titleClassName && "text-3xl font-bold sm:text-4xl lg:text-5xl",
|
||||||
)} style={imageStyle}>
|
titleClassName
|
||||||
<img
|
)}
|
||||||
src={image}
|
style={titleStyle}
|
||||||
alt={title || 'Section Image'}
|
>
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
{title}
|
||||||
/>
|
</h2>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Content Side */}
|
{text && (
|
||||||
<div className={cn('flex flex-col', hasImage ? 'bg-transparent' : '')}>
|
<div
|
||||||
{title && (
|
className={cn(
|
||||||
<h2
|
'prose prose-lg max-w-none w-full',
|
||||||
className={cn(
|
'prose-h1:text-3xl md:prose-h1:text-4xl lg:prose-h1:text-5xl prose-h1:font-bold prose-h1:mt-6 prose-h1:mb-4',
|
||||||
"tracking-tight text-current mb-6",
|
'prose-h2:text-2xl md:prose-h2:text-3xl lg:prose-h2:text-4xl prose-h2:font-bold prose-h2:mt-5 prose-h2:mb-3',
|
||||||
!titleClassName && "text-3xl font-bold sm:text-4xl lg:text-5xl",
|
'prose-h3:text-xl md:prose-h3:text-2xl lg:prose-h3:text-3xl prose-h3:font-bold prose-h3:mt-4 prose-h3:mb-2',
|
||||||
titleClassName
|
'prose-headings:text-[var(--tw-prose-headings)]',
|
||||||
)}
|
'prose-p:text-[var(--tw-prose-body)]',
|
||||||
style={titleStyle}
|
'text-[var(--tw-prose-body)]',
|
||||||
>
|
className,
|
||||||
{title}
|
textClassName
|
||||||
</h2>
|
)}
|
||||||
)}
|
style={proseStyle}
|
||||||
|
dangerouslySetInnerHTML={{ __html: text }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{text && (
|
{/* Buttons */}
|
||||||
<div
|
{buttons && buttons.length > 0 && (
|
||||||
className={cn(
|
<div className="mt-8 flex flex-wrap gap-4">
|
||||||
'prose prose-lg max-w-none',
|
{buttons.map((btn, idx) => (
|
||||||
'prose-h1:text-3xl md:prose-h1:text-4xl lg:prose-h1:text-5xl prose-h1:font-bold prose-h1:mt-6 prose-h1:mb-4',
|
btn.text && btn.url && (
|
||||||
'prose-h2:text-2xl md:prose-h2:text-3xl lg:prose-h2:text-4xl prose-h2:font-bold prose-h2:mt-5 prose-h2:mb-3',
|
<a
|
||||||
'prose-h3:text-xl md:prose-h3:text-2xl lg:prose-h3:text-3xl prose-h3:font-bold prose-h3:mt-4 prose-h3:mb-2',
|
key={idx}
|
||||||
'prose-headings:text-[var(--tw-prose-headings)]',
|
href={btn.url}
|
||||||
'prose-p:text-[var(--tw-prose-body)]',
|
className={cn(
|
||||||
'text-[var(--tw-prose-body)]',
|
"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",
|
||||||
className,
|
!buttonStyle?.style?.backgroundColor && "bg-primary",
|
||||||
textClassName
|
!buttonStyle?.style?.color && "text-primary-foreground hover:bg-primary/90",
|
||||||
)}
|
buttonStyle?.classNames
|
||||||
style={proseStyle}
|
)}
|
||||||
dangerouslySetInnerHTML={{ __html: text }}
|
style={buttonStyle?.style}
|
||||||
/>
|
>
|
||||||
)}
|
{btn.text}
|
||||||
|
</a>
|
||||||
{/* Buttons */}
|
)
|
||||||
{buttons && buttons.length > 0 && (
|
))}
|
||||||
<div className="mt-8 flex flex-wrap gap-4">
|
</div>
|
||||||
{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>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { api } from '@/lib/api/client';
|
import { api } from '@/lib/api/client';
|
||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Section Components
|
// Section Components
|
||||||
import { HeroSection } from './sections/HeroSection';
|
import { HeroSection } from './sections/HeroSection';
|
||||||
@@ -30,7 +31,10 @@ interface SectionStyles {
|
|||||||
backgroundOverlay?: number;
|
backgroundOverlay?: number;
|
||||||
paddingTop?: string;
|
paddingTop?: string;
|
||||||
paddingBottom?: string;
|
paddingBottom?: string;
|
||||||
contentWidth?: 'full' | 'contained';
|
contentWidth?: 'full' | 'contained' | 'boxed';
|
||||||
|
gradientAngle?: number;
|
||||||
|
gradientFrom?: string;
|
||||||
|
gradientTo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ElementStyle {
|
interface ElementStyle {
|
||||||
@@ -266,15 +270,21 @@ export function DynamicPageRenderer({ slug: propSlug }: DynamicPageRendererProps
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={section.id}
|
key={section.id}
|
||||||
className="relative overflow-hidden"
|
className={cn(
|
||||||
|
"relative overflow-hidden",
|
||||||
|
!section.styles?.backgroundColor && !section.styles?.backgroundType && "bg-white/50"
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
// Only explicit custom padding overrides from the padding fields
|
...(section.styles?.backgroundType === 'gradient'
|
||||||
|
? { background: `linear-gradient(${section.styles?.gradientAngle ?? 135}deg, ${section.styles?.gradientFrom || '#9333ea'}, ${section.styles?.gradientTo || '#3b82f6'})` }
|
||||||
|
: { backgroundColor: section.styles?.backgroundColor }
|
||||||
|
),
|
||||||
paddingTop: section.styles?.paddingTop,
|
paddingTop: section.styles?.paddingTop,
|
||||||
paddingBottom: section.styles?.paddingBottom,
|
paddingBottom: section.styles?.paddingBottom,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Full-bleed background image & overlay */}
|
{/* Full-bleed background image & overlay */}
|
||||||
{section.styles?.backgroundImage && (section.styles.backgroundType === 'image' || !section.styles.backgroundType) && (
|
{section.styles?.backgroundType === 'image' && section.styles?.backgroundImage && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat"
|
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat"
|
||||||
@@ -286,19 +296,51 @@ export function DynamicPageRenderer({ slug: propSlug }: DynamicPageRendererProps
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/* Legacy: show bg image even without backgroundType set */}
|
||||||
|
{!section.styles?.backgroundType && 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 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Section component — manages its own background, height, and inner content width */}
|
{/* Content Wrapper */}
|
||||||
<div className="relative z-10 w-full">
|
{section.styles?.contentWidth === 'boxed' ? (
|
||||||
<SectionComponent
|
<div className="relative z-10 container mx-auto px-4 max-w-5xl">
|
||||||
id={section.id}
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
section={section}
|
<SectionComponent
|
||||||
layout={section.layoutVariant || 'default'}
|
id={section.id}
|
||||||
colorScheme={section.colorScheme || 'default'}
|
section={section}
|
||||||
styles={section.styles}
|
layout={section.layoutVariant || 'default'}
|
||||||
elementStyles={section.elementStyles}
|
colorScheme={section.colorScheme || 'default'}
|
||||||
{...flattenSectionProps(section.props || {})}
|
styles={section.styles}
|
||||||
/>
|
elementStyles={section.elementStyles}
|
||||||
</div>
|
{...flattenSectionProps(section.props || {})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={cn(
|
||||||
|
"relative z-10 w-full",
|
||||||
|
section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : ''
|
||||||
|
)}>
|
||||||
|
<SectionComponent
|
||||||
|
id={section.id}
|
||||||
|
section={section}
|
||||||
|
layout={section.layoutVariant || 'default'}
|
||||||
|
colorScheme={section.colorScheme || 'default'}
|
||||||
|
styles={section.styles}
|
||||||
|
elementStyles={section.elementStyles}
|
||||||
|
{...flattenSectionProps(section.props || {})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
|
|
||||||
interface BentoItem {
|
interface BentoItem {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -57,7 +58,6 @@ export function BentoCategoryGrid({
|
|||||||
styles,
|
styles,
|
||||||
elementStyles,
|
elementStyles,
|
||||||
}: BentoCategoryGridProps) {
|
}: BentoCategoryGridProps) {
|
||||||
const sectionBg = getSectionBackground(styles);
|
|
||||||
// Keep initial demo layout stable: merge configured items over demo items by index.
|
// Keep initial demo layout stable: merge configured items over demo items by index.
|
||||||
// This prevents the preview grid from "collapsing" when the first item is added.
|
// This prevents the preview grid from "collapsing" when the first item is added.
|
||||||
const displayItems: BentoItem[] = (() => {
|
const displayItems: BentoItem[] = (() => {
|
||||||
@@ -69,13 +69,17 @@ export function BentoCategoryGrid({
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
const hasCustomPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id={id}
|
id={id}
|
||||||
className="wn-section wn-bento-grid py-12 md:py-16"
|
className={cn("wn-section wn-bento-grid relative overflow-hidden w-full", hasCustomPadding ? "" : "py-12 md:py-16")}
|
||||||
style={sectionBg.style}
|
style={sectionBg.style}
|
||||||
>
|
>
|
||||||
<div className="container mx-auto px-4 max-w-7xl">
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
|
<div className="w-full mx-auto px-4 relative z-10">
|
||||||
{title && (
|
{title && (
|
||||||
<h2
|
<h2
|
||||||
className="text-3xl md:text-4xl font-bold mb-8"
|
className="text-3xl md:text-4xl font-bold mb-8"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
|
|
||||||
interface CTABannerSectionProps {
|
interface CTABannerSectionProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,7 +66,7 @@ export function CTABannerSection({
|
|||||||
{title && (
|
{title && (
|
||||||
<h2
|
<h2
|
||||||
className={cn(
|
className={cn(
|
||||||
"wn-cta__title mb-6",
|
"wn-cta__title mb-6 w-full",
|
||||||
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl lg:text-5xl",
|
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl lg:text-5xl",
|
||||||
!elementStyles?.title?.fontWeight && "font-bold",
|
!elementStyles?.title?.fontWeight && "font-bold",
|
||||||
titleStyle.classNames
|
titleStyle.classNames
|
||||||
@@ -78,7 +79,7 @@ export function CTABannerSection({
|
|||||||
|
|
||||||
{text && (
|
{text && (
|
||||||
<p className={cn(
|
<p className={cn(
|
||||||
'wn-cta-banner__text mb-8 max-w-2xl mx-auto',
|
'wn-cta-banner__text mb-8 max-w-2xl mx-auto w-full',
|
||||||
!elementStyles?.text?.fontSize && "text-lg md:text-xl",
|
!elementStyles?.text?.fontSize && "text-lg md:text-xl",
|
||||||
styles?.contentWidth !== 'boxed' && {
|
styles?.contentWidth !== 'boxed' && {
|
||||||
'text-white/90': colorScheme === 'primary',
|
'text-white/90': colorScheme === 'primary',
|
||||||
@@ -120,51 +121,23 @@ export function CTABannerSection({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
|
||||||
const sectionBg = getSectionBackground(styles);
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
|
||||||
// Helper to get background style for dynamic schemes
|
|
||||||
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
|
||||||
if (hasCustomBackground) return sectionBg.style;
|
|
||||||
if (colorScheme === 'primary') {
|
|
||||||
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
|
||||||
}
|
|
||||||
if (colorScheme === 'secondary') {
|
|
||||||
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'wn-section wn-cta-banner',
|
'wn-section wn-cta-banner relative overflow-hidden w-full',
|
||||||
`wn-cta-banner--${layout}`,
|
`wn-cta-banner--${layout}`,
|
||||||
`wn-scheme--${colorScheme}`,
|
`wn-scheme--${colorScheme}`,
|
||||||
heightClasses,
|
heightClasses
|
||||||
{
|
|
||||||
'bg-primary text-primary-foreground': colorScheme === 'primary' && !hasCustomBackground,
|
|
||||||
'bg-secondary text-secondary-foreground': colorScheme === 'secondary' && !hasCustomBackground,
|
|
||||||
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
style={getBackgroundStyle()}
|
style={sectionBg.style}
|
||||||
>
|
>
|
||||||
{styles?.contentWidth === 'boxed' ? (
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
<div className="container mx-auto px-4 max-w-5xl">
|
<div className="mx-auto px-4 text-center relative z-10 w-full">
|
||||||
<div className="bg-white text-gray-900 rounded-2xl shadow-sm border border-gray-200 overflow-hidden px-6 md:px-10 py-10 text-center">
|
{innerContent}
|
||||||
{innerContent}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={cn(
|
|
||||||
"mx-auto px-4 text-center",
|
|
||||||
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
|
||||||
)}>
|
|
||||||
{innerContent}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
|
|
||||||
interface ContactFormSectionProps {
|
interface ContactFormSectionProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -97,40 +98,22 @@ export function ContactFormSection({
|
|||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
}; const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
|
||||||
const sectionBg = getSectionBackground(styles);
|
|
||||||
|
|
||||||
// Helper to get background style for dynamic schemes
|
|
||||||
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
|
||||||
if (hasCustomBackground) return sectionBg.style;
|
|
||||||
if (colorScheme === 'primary') {
|
|
||||||
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
|
||||||
}
|
|
||||||
if (colorScheme === 'secondary') {
|
|
||||||
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'wn-section wn-contact-form',
|
'wn-section wn-contact-form relative overflow-hidden w-full',
|
||||||
`wn-scheme--${colorScheme}`,
|
`wn-scheme--${colorScheme}`,
|
||||||
heightClasses,
|
heightClasses
|
||||||
{
|
|
||||||
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
style={getBackgroundStyle()}
|
style={sectionBg.style}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
"mx-auto px-4",
|
<div className="mx-auto px-4 relative z-10 w-full">
|
||||||
styles?.contentWidth === 'full' ? 'w-full'
|
|
||||||
: styles?.contentWidth === 'boxed' ? 'max-w-5xl'
|
|
||||||
: 'container'
|
|
||||||
)}>
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'max-w-xl mx-auto',
|
'max-w-xl mx-auto',
|
||||||
{
|
{
|
||||||
@@ -139,7 +122,7 @@ export function ContactFormSection({
|
|||||||
)}>
|
)}>
|
||||||
{title && (
|
{title && (
|
||||||
<h2 className={cn(
|
<h2 className={cn(
|
||||||
"wn-contact__title text-center mb-12",
|
"wn-contact__title text-center mb-12 w-full",
|
||||||
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl lg:text-5xl",
|
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl lg:text-5xl",
|
||||||
!elementStyles?.title?.fontWeight && "font-bold",
|
!elementStyles?.title?.fontWeight && "font-bold",
|
||||||
titleStyle.classNames
|
titleStyle.classNames
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
|
|
||||||
interface ContentSectionProps {
|
interface ContentSectionProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -170,10 +171,12 @@ export function ContentSection({ section, content: propContent, cta_text: propCt
|
|||||||
'small': 'py-8 md:py-12',
|
'small': 'py-8 md:py-12',
|
||||||
'medium': 'py-16 md:py-24',
|
'medium': 'py-16 md:py-24',
|
||||||
'large': 'py-24 md:py-32',
|
'large': 'py-24 md:py-32',
|
||||||
'screen': 'min-h-screen py-20 flex items-center',
|
'fullscreen': 'min-h-screen py-20 flex items-center',
|
||||||
};
|
};
|
||||||
|
|
||||||
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-20';
|
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-20';
|
||||||
|
const sectionBg = getSectionBackground(section.styles);
|
||||||
|
const finalHeightClasses = (section.styles?.paddingTop || section.styles?.paddingBottom) ? '' : heightClasses;
|
||||||
|
|
||||||
const content = propContent || section.props?.content?.value || section.props?.content || '';
|
const content = propContent || section.props?.content?.value || section.props?.content || '';
|
||||||
|
|
||||||
@@ -206,34 +209,19 @@ export function ContentSection({ section, content: propContent, cta_text: propCt
|
|||||||
const cta_text = propCtaText || section.props?.cta_text?.value || section.props?.cta_text;
|
const cta_text = propCtaText || section.props?.cta_text?.value || section.props?.cta_text;
|
||||||
const cta_url = propCtaUrl || section.props?.cta_url?.value || section.props?.cta_url;
|
const cta_url = propCtaUrl || section.props?.cta_url?.value || section.props?.cta_url;
|
||||||
|
|
||||||
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
|
||||||
const sectionBg = getSectionBackground(section.styles);
|
|
||||||
|
|
||||||
// Helper to get background style for dynamic schemes
|
|
||||||
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
|
||||||
if (hasCustomBackground) return sectionBg.style;
|
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<style dangerouslySetInnerHTML={{ __html: generateScopedStyles(section.id, section.elementStyles || {}) }} />
|
<style dangerouslySetInnerHTML={{ __html: generateScopedStyles(section.id, section.elementStyles || {}) }} />
|
||||||
<section
|
<section
|
||||||
id={section.id}
|
id={section.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'wn-content',
|
'wn-content relative w-full overflow-hidden',
|
||||||
heightClasses,
|
finalHeightClasses,
|
||||||
!hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg,
|
|
||||||
scheme.text
|
scheme.text
|
||||||
)}
|
)}
|
||||||
style={getBackgroundStyle()}
|
style={sectionBg.style}
|
||||||
>
|
>
|
||||||
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
<SharedContentLayout
|
<SharedContentLayout
|
||||||
text={content}
|
text={content}
|
||||||
textStyle={textStyle.style}
|
textStyle={textStyle.style}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import * as LucideIcons from 'lucide-react';
|
import * as LucideIcons from 'lucide-react';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
|
|
||||||
interface FeatureItem {
|
interface FeatureItem {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -78,43 +79,23 @@ export function FeatureGridSection({
|
|||||||
|
|
||||||
const headingStyle = getTextStyles('heading');
|
const headingStyle = getTextStyles('heading');
|
||||||
const featureItemStyle = getTextStyles('feature_item');
|
const featureItemStyle = getTextStyles('feature_item');
|
||||||
|
const linkStyle = getTextStyles('link');
|
||||||
|
|
||||||
const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
|
||||||
const sectionBg = getSectionBackground(styles);
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
|
||||||
// Helper to get background style for dynamic schemes
|
|
||||||
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
|
||||||
if (hasCustomBackground) return sectionBg.style;
|
|
||||||
if (colorScheme === 'primary') {
|
|
||||||
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
|
||||||
}
|
|
||||||
if (colorScheme === 'secondary') {
|
|
||||||
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'wn-section wn-feature-grid',
|
'wn-section wn-feature-grid relative overflow-hidden w-full',
|
||||||
`wn-feature-grid--${layout}`,
|
`wn-feature-grid--${layout}`,
|
||||||
`wn-scheme--${colorScheme}`,
|
`wn-scheme--${colorScheme}`,
|
||||||
heightClasses,
|
heightClasses
|
||||||
{
|
|
||||||
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
|
||||||
'text-primary-foreground': colorScheme === 'primary' && !hasCustomBackground,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
style={getBackgroundStyle()}
|
style={sectionBg.style}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
"mx-auto px-4",
|
<div className="mx-auto px-4 relative z-10 w-full">
|
||||||
styles?.contentWidth === 'full' ? 'w-full'
|
|
||||||
: styles?.contentWidth === 'boxed' ? 'max-w-5xl'
|
|
||||||
: 'container'
|
|
||||||
)}>
|
|
||||||
{heading && (
|
{heading && (
|
||||||
<h2
|
<h2
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -167,16 +148,22 @@ export function FeatureGridSection({
|
|||||||
<p className="text-xs text-gray-400 mb-2 uppercase tracking-wider">{item.date}</p>
|
<p className="text-xs text-gray-400 mb-2 uppercase tracking-wider">{item.date}</p>
|
||||||
)}
|
)}
|
||||||
{item.title && (
|
{item.title && (
|
||||||
<h3 className="font-semibold text-gray-900 text-base leading-snug mb-2 group-hover:text-primary transition-colors line-clamp-2">
|
<h3 className={cn("font-semibold text-gray-900 leading-snug mb-2 group-hover:text-primary transition-colors line-clamp-2 w-full",
|
||||||
|
!elementStyles?.feature_item?.fontSize && "text-base",
|
||||||
|
featureItemStyle.classNames
|
||||||
|
)} style={featureItemStyle.style}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
)}
|
)}
|
||||||
{(item.excerpt || item.description) && (
|
{(item.excerpt || item.description) && (
|
||||||
<p className="text-sm text-gray-500 line-clamp-3 mb-4">
|
<p className={cn("text-gray-500 line-clamp-3 mb-4 w-full",
|
||||||
|
!elementStyles?.feature_item?.fontSize && "text-sm",
|
||||||
|
featureItemStyle.classNames
|
||||||
|
)} style={featureItemStyle.style}>
|
||||||
{item.excerpt || item.description}
|
{item.excerpt || item.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<span className="inline-flex items-center gap-1 text-sm font-medium text-primary">
|
<span className={cn("inline-flex items-center gap-1 text-sm font-medium text-primary", linkStyle.classNames)} style={linkStyle.style}>
|
||||||
Read more
|
Read more
|
||||||
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
|
|
||||||
interface HeroSectionProps {
|
interface HeroSectionProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -89,16 +90,14 @@ export function HeroSection({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'wn-section wn-hero',
|
'wn-section wn-hero',
|
||||||
`wn-hero--${layout}`,
|
`wn-hero--${layout}`,
|
||||||
'relative overflow-hidden',
|
'relative overflow-hidden w-full',
|
||||||
heightClasses,
|
heightClasses,
|
||||||
)}
|
)}
|
||||||
style={sectionBg.style}
|
style={sectionBg.style}
|
||||||
>
|
>
|
||||||
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'mx-auto px-4 z-10 relative flex w-full',
|
'mx-auto z-10 relative flex w-full',
|
||||||
styles?.contentWidth === 'full' ? 'w-full'
|
|
||||||
: styles?.contentWidth === 'boxed' ? 'max-w-5xl'
|
|
||||||
: 'container max-w-7xl',
|
|
||||||
{
|
{
|
||||||
'flex flex-col md:flex-row items-center gap-8': isImageLeft || isImageRight,
|
'flex flex-col md:flex-row items-center gap-8': isImageLeft || isImageRight,
|
||||||
'text-center': isCentered,
|
'text-center': isCentered,
|
||||||
@@ -108,11 +107,15 @@ export function HeroSection({
|
|||||||
{image && isImageLeft && (
|
{image && isImageLeft && (
|
||||||
<div className="w-full md:w-1/2">
|
<div className="w-full md:w-1/2">
|
||||||
<div
|
<div
|
||||||
className="rounded-lg shadow-xl overflow-hidden"
|
className={cn("shadow-xl overflow-hidden", !imageStyle.borderRadius && "rounded-lg")}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: imageStyle.backgroundColor,
|
backgroundColor: imageStyle.backgroundColor,
|
||||||
width: imageStyle.width || 'auto',
|
width: imageStyle.width || 'auto',
|
||||||
maxWidth: '100%'
|
maxWidth: '100%',
|
||||||
|
borderRadius: imageStyle.borderRadius,
|
||||||
|
borderColor: imageStyle.borderColor,
|
||||||
|
borderWidth: imageStyle.borderWidth,
|
||||||
|
borderStyle: imageStyle.borderWidth ? 'solid' : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -139,7 +142,7 @@ export function HeroSection({
|
|||||||
{title && (
|
{title && (
|
||||||
<h1
|
<h1
|
||||||
className={cn(
|
className={cn(
|
||||||
"wn-hero__title mb-6 leading-tight",
|
"wn-hero__title mb-6 leading-tight w-full",
|
||||||
!elementStyles?.title?.fontSize && "text-4xl md:text-5xl lg:text-6xl",
|
!elementStyles?.title?.fontSize && "text-4xl md:text-5xl lg:text-6xl",
|
||||||
!elementStyles?.title?.fontWeight && "font-bold",
|
!elementStyles?.title?.fontWeight && "font-bold",
|
||||||
titleStyle.classNames
|
titleStyle.classNames
|
||||||
@@ -153,7 +156,7 @@ export function HeroSection({
|
|||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"wn-hero__subtitle text-opacity-80 mb-8",
|
"wn-hero__subtitle text-opacity-80 mb-8 w-full",
|
||||||
!elementStyles?.subtitle?.fontSize && "text-lg md:text-xl",
|
!elementStyles?.subtitle?.fontSize && "text-lg md:text-xl",
|
||||||
subtitleStyle.classNames
|
subtitleStyle.classNames
|
||||||
)}
|
)}
|
||||||
@@ -166,10 +169,19 @@ export function HeroSection({
|
|||||||
{/* Centered Image */}
|
{/* Centered Image */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-12 mx-auto rounded-lg shadow-xl overflow-hidden",
|
"mt-12 mx-auto shadow-xl overflow-hidden",
|
||||||
|
!imageStyle.borderRadius && "rounded-lg",
|
||||||
imageStyle.width ? "" : "max-w-4xl"
|
imageStyle.width ? "" : "max-w-4xl"
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: imageStyle.backgroundColor, width: imageStyle.width || 'auto', maxWidth: '100%' }}
|
style={{
|
||||||
|
backgroundColor: imageStyle.backgroundColor,
|
||||||
|
width: imageStyle.width || 'auto',
|
||||||
|
maxWidth: '100%',
|
||||||
|
borderRadius: imageStyle.borderRadius,
|
||||||
|
borderColor: imageStyle.borderColor,
|
||||||
|
borderWidth: imageStyle.borderWidth,
|
||||||
|
borderStyle: imageStyle.borderWidth ? 'solid' : undefined,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{image && isCentered && (
|
{image && isCentered && (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
|
|
||||||
interface ImageTextSectionProps {
|
interface ImageTextSectionProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -55,6 +56,8 @@ export function ImageTextSection({
|
|||||||
|
|
||||||
const imageStyle = elementStyles?.['image'] || {};
|
const imageStyle = elementStyles?.['image'] || {};
|
||||||
|
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
|
||||||
// Height preset support
|
// Height preset support
|
||||||
const heightPreset = styles?.heightPreset || 'default';
|
const heightPreset = styles?.heightPreset || 'default';
|
||||||
const heightMap: Record<string, string> = {
|
const heightMap: Record<string, string> = {
|
||||||
@@ -62,38 +65,21 @@ export function ImageTextSection({
|
|||||||
'small': 'py-8 md:py-16',
|
'small': 'py-8 md:py-16',
|
||||||
'medium': 'py-16 md:py-32',
|
'medium': 'py-16 md:py-32',
|
||||||
'large': 'py-24 md:py-48',
|
'large': 'py-24 md:py-48',
|
||||||
'screen': 'min-h-screen py-20 flex items-center',
|
'fullscreen': 'min-h-screen py-20 flex items-center',
|
||||||
};
|
};
|
||||||
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-24';
|
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-24';
|
||||||
|
|
||||||
const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
|
||||||
const sectionBg = getSectionBackground(styles);
|
|
||||||
|
|
||||||
// Helper to get background style for dynamic schemes
|
|
||||||
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
|
||||||
if (hasCustomBackground) return sectionBg.style;
|
|
||||||
if (colorScheme === 'primary') {
|
|
||||||
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
|
||||||
}
|
|
||||||
if (colorScheme === 'secondary') {
|
|
||||||
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'wn-section wn-image-text',
|
'wn-section wn-image-text relative overflow-hidden w-full',
|
||||||
`wn-scheme--${colorScheme}`,
|
`wn-scheme--${colorScheme}`,
|
||||||
!styles?.paddingTop && !styles?.paddingBottom && heightClasses,
|
!styles?.paddingTop && !styles?.paddingBottom && heightClasses
|
||||||
{
|
|
||||||
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
style={getBackgroundStyle()}
|
style={sectionBg.style}
|
||||||
>
|
>
|
||||||
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
<SharedContentLayout
|
<SharedContentLayout
|
||||||
title={title}
|
title={title}
|
||||||
text={text}
|
text={text}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
|
|
||||||
interface MarqueeBannerProps {
|
interface MarqueeBannerProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,19 +18,26 @@ export function MarqueeBanner({
|
|||||||
separator = '✦',
|
separator = '✦',
|
||||||
styles,
|
styles,
|
||||||
}: MarqueeBannerProps) {
|
}: MarqueeBannerProps) {
|
||||||
const sectionBg = getSectionBackground(styles);
|
|
||||||
const items = text.split(separator).map(t => t.trim()).filter(Boolean);
|
const items = text.split(separator).map(t => t.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
// If the parent didn't set a custom background, we fallback to primary for the marquee.
|
||||||
|
const hasCustomBg = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
||||||
|
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
const hasCustomPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id={id}
|
id={id}
|
||||||
className="wn-section wn-marquee overflow-hidden py-3"
|
className={cn("wn-section wn-marquee relative overflow-hidden w-full", hasCustomPadding ? "" : "py-3")}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: sectionBg.style?.backgroundColor || 'var(--wn-primary, #1a1a1a)',
|
...sectionBg.style,
|
||||||
color: sectionBg.style?.color || '#fff',
|
backgroundColor: !hasCustomBg ? 'var(--wn-primary, #1a1a1a)' : sectionBg.style.backgroundColor,
|
||||||
|
color: !hasCustomBg ? '#fff' : 'inherit',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex whitespace-nowrap">
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
|
<div className="flex whitespace-nowrap relative z-10">
|
||||||
{/* Duplicate twice for seamless infinite scroll */}
|
{/* Duplicate twice for seamless infinite scroll */}
|
||||||
{[0, 1].map((i) => (
|
{[0, 1].map((i) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { apiClient } from '@/lib/api/client';
|
|||||||
import { ProductCard } from '@/components/ProductCard';
|
import { ProductCard } from '@/components/ProductCard';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
import type { ProductsResponse } from '@/types/product';
|
import type { ProductsResponse } from '@/types/product';
|
||||||
|
|
||||||
interface ProductCarouselProps {
|
interface ProductCarouselProps {
|
||||||
@@ -39,7 +40,6 @@ export function ProductCarousel({
|
|||||||
elementStyles,
|
elementStyles,
|
||||||
}: ProductCarouselProps) {
|
}: ProductCarouselProps) {
|
||||||
const trackRef = useRef<HTMLDivElement>(null);
|
const trackRef = useRef<HTMLDivElement>(null);
|
||||||
const sectionBg = getSectionBackground(styles);
|
|
||||||
|
|
||||||
// Build query params
|
// Build query params
|
||||||
const queryParams = new URLSearchParams({ per_page: String(limit) });
|
const queryParams = new URLSearchParams({ per_page: String(limit) });
|
||||||
@@ -68,29 +68,64 @@ export function ProductCarousel({
|
|||||||
trackRef.current.scrollBy({ left: direction === 'left' ? -cardWidth * 2 : cardWidth * 2, behavior: 'smooth' });
|
trackRef.current.scrollBy({ left: direction === 'left' ? -cardWidth * 2 : cardWidth * 2, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to get text styles (including font family)
|
||||||
|
const getTextStyles = (elementName: string) => {
|
||||||
|
const elementStyle = elementStyles?.[elementName] || {};
|
||||||
|
return {
|
||||||
|
classNames: cn(
|
||||||
|
elementStyle.fontSize,
|
||||||
|
elementStyle.fontWeight,
|
||||||
|
{
|
||||||
|
'font-sans': elementStyle.fontFamily === 'secondary',
|
||||||
|
'font-serif': elementStyle.fontFamily === 'primary',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
style: {
|
||||||
|
color: elementStyle.color,
|
||||||
|
textAlign: elementStyle.textAlign,
|
||||||
|
backgroundColor: elementStyle.backgroundColor,
|
||||||
|
borderColor: elementStyle.borderColor,
|
||||||
|
borderWidth: elementStyle.borderWidth,
|
||||||
|
borderRadius: elementStyle.borderRadius,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleStyle = getTextStyles('title');
|
||||||
|
const subtitleStyle = getTextStyles('subtitle');
|
||||||
|
const linkStyle = getTextStyles('link');
|
||||||
|
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
const hasCustomPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={id} className="wn-section wn-product-carousel py-12 md:py-16" style={sectionBg.style}>
|
<section
|
||||||
<div className="container mx-auto px-4 max-w-7xl">
|
id={id}
|
||||||
|
className={cn("wn-section wn-product-carousel relative overflow-hidden w-full", hasCustomPadding ? "" : "py-12 md:py-16")}
|
||||||
|
style={sectionBg.style}
|
||||||
|
>
|
||||||
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
|
<div className="w-full mx-auto px-4 relative z-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-end justify-between mb-8">
|
<div className="flex items-end justify-between mb-8">
|
||||||
<div>
|
<div>
|
||||||
{title && (
|
{title && (
|
||||||
<h2
|
<h2
|
||||||
className="text-3xl md:text-4xl font-bold"
|
className={cn("text-3xl md:text-4xl font-bold", titleStyle.classNames)}
|
||||||
style={{ color: elementStyles?.title?.color }}
|
style={titleStyle.style}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p className="text-muted-foreground mt-2" style={{ color: elementStyles?.subtitle?.color }}>
|
<p className={cn("text-muted-foreground mt-2", subtitleStyle.classNames)} style={subtitleStyle.style}>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{cta_text && cta_url && (
|
{cta_text && cta_url && (
|
||||||
<Link to={cta_url} className="text-sm font-semibold hover:underline mr-4 whitespace-nowrap">
|
<Link to={cta_url} className={cn("text-sm font-semibold hover:underline mr-4 whitespace-nowrap", linkStyle.classNames)} style={linkStyle.style}>
|
||||||
{cta_text} →
|
{cta_text} →
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { X, ShoppingCart, Eye } from 'lucide-react';
|
import { X, ShoppingCart, Eye } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getSectionBackground } from '@/lib/sectionStyles';
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
|
||||||
import { formatPrice } from '@/lib/currency';
|
import { formatPrice } from '@/lib/currency';
|
||||||
import { useCartStore } from '@/lib/cart/store';
|
import { useCartStore } from '@/lib/cart/store';
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { apiClient } from '@/lib/api/client';
|
||||||
@@ -47,7 +48,6 @@ export function ShoppableImage({
|
|||||||
styles,
|
styles,
|
||||||
elementStyles,
|
elementStyles,
|
||||||
}: ShoppableImageProps) {
|
}: ShoppableImageProps) {
|
||||||
const sectionBg = getSectionBackground(styles);
|
|
||||||
const [activeHotspot, setActiveHotspot] = useState<number | null>(null);
|
const [activeHotspot, setActiveHotspot] = useState<number | null>(null);
|
||||||
const { addItem, openCart } = useCartStore();
|
const { addItem, openCart } = useCartStore();
|
||||||
|
|
||||||
@@ -81,9 +81,17 @@ export function ShoppableImage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
const hasCustomPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={id} className="wn-section wn-shoppable-image py-12 md:py-16" style={sectionBg.style}>
|
<section
|
||||||
<div className="container mx-auto px-4 max-w-7xl">
|
id={id}
|
||||||
|
className={cn("wn-section wn-shoppable-image relative overflow-hidden w-full", hasCustomPadding ? "" : "py-12 md:py-16")}
|
||||||
|
style={sectionBg.style}
|
||||||
|
>
|
||||||
|
<SectionBackgroundRenderer bg={sectionBg} />
|
||||||
|
<div className="w-full mx-auto px-4 relative z-10">
|
||||||
{(title || subtitle) && (
|
{(title || subtitle) && (
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
{title && (
|
{title && (
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: ["./src/**/*.{ts,tsx,css}", "./components/**/*.{ts,tsx,css}"],
|
content: ["./src/**/*.{ts,tsx,css}", "./components/**/*.{ts,tsx,css}"],
|
||||||
|
safelist: [
|
||||||
|
// Dynamic typography classes selected via Inspector Panel
|
||||||
|
'text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'text-5xl', 'text-6xl',
|
||||||
|
'font-light', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'font-extrabold'
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
container: { center: true, padding: "1rem" },
|
container: { center: true, padding: "1rem" },
|
||||||
extend: {
|
extend: {
|
||||||
|
|||||||
@@ -446,11 +446,15 @@ class PagesController
|
|||||||
$rendered_sections = self::resolve_sections_for_post($template['sections'], $post, $type);
|
$rendered_sections = self::resolve_sections_for_post($template['sections'], $post, $type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get SPA settings
|
||||||
|
$settings = get_option('woonoow_appearance_settings', []);
|
||||||
|
|
||||||
return new WP_REST_Response([
|
return new WP_REST_Response([
|
||||||
'type' => 'content',
|
'type' => 'content',
|
||||||
'cpt' => $type,
|
'cpt' => $type,
|
||||||
'post' => $post_data,
|
'post' => $post_data,
|
||||||
'seo' => $seo,
|
'seo' => $seo,
|
||||||
|
'effective_container_width' => ($settings['general']['container_width'] ?? 'boxed') ?: 'boxed',
|
||||||
'template' => $template ?: ['sections' => []],
|
'template' => $template ?: ['sections' => []],
|
||||||
'rendered' => [
|
'rendered' => [
|
||||||
'sections' => $rendered_sections,
|
'sections' => $rendered_sections,
|
||||||
|
|||||||
Reference in New Issue
Block a user