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
|
||||
className={cn(
|
||||
'bg-white transition-all duration-300 min-h-[500px]',
|
||||
'bg-white transition-all duration-300 min-h-[500px] wn-page',
|
||||
deviceMode === 'mobile'
|
||||
? '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 ? (
|
||||
|
||||
@@ -159,8 +159,8 @@ export function CanvasSection({
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn(
|
||||
"relative z-10",
|
||||
section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : 'w-full'
|
||||
"relative z-10 w-full",
|
||||
section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : ''
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api';
|
||||
import { __ } from '@/lib/i18n';
|
||||
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 {
|
||||
AlertDialog,
|
||||
@@ -50,6 +50,11 @@ export default function AppearancePages() {
|
||||
setInspectorCollapsed,
|
||||
setAvailableSources,
|
||||
setIsLoading,
|
||||
undo,
|
||||
redo,
|
||||
past,
|
||||
future,
|
||||
updateCurrentPage,
|
||||
addSection,
|
||||
deleteSection,
|
||||
duplicateSection,
|
||||
@@ -61,6 +66,7 @@ export default function AppearancePages() {
|
||||
updateSectionStyles,
|
||||
updateElementStyles,
|
||||
markAsSaved,
|
||||
markAsChanged,
|
||||
setAsSpaLanding,
|
||||
unsetSpaLanding,
|
||||
} = usePageEditorStore();
|
||||
@@ -160,7 +166,10 @@ export default function AppearancePages() {
|
||||
const endpoint = currentPage.type === 'page'
|
||||
? `/pages/${currentPage.slug}`
|
||||
: `/templates/${currentPage.cpt}`;
|
||||
return api.post(endpoint, { sections });
|
||||
return api.post(endpoint, {
|
||||
sections,
|
||||
container_width: currentPage.containerWidth
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
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" />}
|
||||
</Button>
|
||||
{hasUnsavedChanges && (
|
||||
<>
|
||||
<span className="text-sm text-amber-600">{__('Unsaved changes')}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDiscard}
|
||||
>
|
||||
<Undo2 className="w-4 h-4 mr-2" />
|
||||
{__('Discard')}
|
||||
</Button>
|
||||
</>
|
||||
<span className="text-sm text-amber-600">{__('Unsaved changes')}</span>
|
||||
)}
|
||||
|
||||
<div className="flex items-center rounded-md border bg-muted/50 p-0.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||
onClick={undo}
|
||||
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
|
||||
variant="outline"
|
||||
@@ -455,10 +487,7 @@ export default function AppearancePages() {
|
||||
onDeletePage={handleDeletePage}
|
||||
onDeleteTemplate={handleDeleteTemplate}
|
||||
onContainerWidthChange={(width) => {
|
||||
if (currentPage) {
|
||||
setCurrentPage({ ...currentPage, containerWidth: width });
|
||||
markAsSaved(); // Mark as changed so save button enables
|
||||
}
|
||||
updateCurrentPage({ containerWidth: width });
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -145,6 +145,7 @@ export const SECTION_SCHEMAS: Record<string, SectionSchema> = {
|
||||
stylableElements: [
|
||||
{ name: 'heading', label: 'Heading', type: 'text' },
|
||||
{ name: 'feature_item', label: 'Feature Item (Card)', type: 'text' },
|
||||
{ name: 'link', label: 'Link (Read more)', type: 'text' },
|
||||
],
|
||||
},
|
||||
'cta-banner': {
|
||||
@@ -233,6 +234,7 @@ export const SECTION_SCHEMAS: Record<string, SectionSchema> = {
|
||||
stylableElements: [
|
||||
{ name: 'title', label: 'Title', type: 'text' },
|
||||
{ name: 'subtitle', label: 'Subtitle', type: 'text' },
|
||||
{ name: 'link', label: 'CTA Link', type: 'text' },
|
||||
],
|
||||
},
|
||||
'shoppable-image': {
|
||||
|
||||
@@ -73,6 +73,10 @@ export interface PageItem {
|
||||
isSpaLanding?: boolean;
|
||||
containerWidth?: 'boxed' | 'fullwidth' | 'default';
|
||||
}
|
||||
interface HistoryState {
|
||||
sections: Section[];
|
||||
currentPage: PageItem | null;
|
||||
}
|
||||
|
||||
interface PageEditorState {
|
||||
// Current page/template being edited
|
||||
@@ -91,6 +95,10 @@ interface PageEditorState {
|
||||
hasUnsavedChanges: boolean;
|
||||
isLoading: boolean;
|
||||
|
||||
// History (Undo/Redo)
|
||||
past: HistoryState[];
|
||||
future: HistoryState[];
|
||||
|
||||
// Available sources for dynamic fields (CPT templates)
|
||||
availableSources: { value: string; label: string }[];
|
||||
|
||||
@@ -104,6 +112,14 @@ interface PageEditorState {
|
||||
setAvailableSources: (sources: { value: string; label: string }[]) => void;
|
||||
setIsLoading: (loading: boolean) => void;
|
||||
|
||||
// History actions
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
pushHistory: () => void;
|
||||
|
||||
// Page updates
|
||||
updateCurrentPage: (updates: Partial<PageItem>) => void;
|
||||
|
||||
// Section actions
|
||||
addSection: (type: string, index?: number) => void;
|
||||
deleteSection: (id: string) => void;
|
||||
@@ -137,11 +153,13 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
inspectorCollapsed: false,
|
||||
hasUnsavedChanges: false,
|
||||
isLoading: false,
|
||||
past: [],
|
||||
future: [],
|
||||
availableSources: [],
|
||||
|
||||
// Setters
|
||||
setCurrentPage: (currentPage) => set({ currentPage }),
|
||||
setSections: (sections) => set({ sections, hasUnsavedChanges: true }),
|
||||
setSections: (sections) => set({ sections, hasUnsavedChanges: true, past: [], future: [] }),
|
||||
setSelectedSection: (selectedSectionId) => set({ selectedSectionId }),
|
||||
setHoveredSection: (hoveredSectionId) => set({ hoveredSectionId }),
|
||||
setDeviceMode: (deviceMode) => set({ deviceMode }),
|
||||
@@ -149,9 +167,64 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
setAvailableSources: (availableSources) => set({ availableSources }),
|
||||
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
|
||||
addSection: (type, index) => {
|
||||
const { sections } = get();
|
||||
const { sections, pushHistory } = get();
|
||||
const sectionConfig = getSectionSchema(type);
|
||||
|
||||
if (!sectionConfig) return;
|
||||
@@ -163,6 +236,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
styles: cloneDefaultStyles(type) as SectionStyles,
|
||||
};
|
||||
|
||||
pushHistory();
|
||||
|
||||
const newSections = [...sections];
|
||||
if (typeof index === 'number') {
|
||||
newSections.splice(index, 0, newSection);
|
||||
@@ -177,7 +252,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
},
|
||||
|
||||
deleteSection: (id) => {
|
||||
const { sections, selectedSectionId } = get();
|
||||
const { sections, selectedSectionId, pushHistory } = get();
|
||||
pushHistory();
|
||||
const newSections = sections.filter(s => s.id !== id);
|
||||
|
||||
set({
|
||||
@@ -188,10 +264,12 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
},
|
||||
|
||||
duplicateSection: (id) => {
|
||||
const { sections } = get();
|
||||
const { sections, pushHistory } = get();
|
||||
const index = sections.findIndex(s => s.id === id);
|
||||
if (index === -1) return;
|
||||
|
||||
pushHistory();
|
||||
|
||||
const section = sections[index];
|
||||
const newSection: Section = {
|
||||
...JSON.parse(JSON.stringify(section)), // Deep clone
|
||||
@@ -205,27 +283,32 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
},
|
||||
|
||||
moveSection: (id, direction) => {
|
||||
const { sections } = get();
|
||||
const { sections, pushHistory } = get();
|
||||
const index = sections.findIndex(s => s.id === id);
|
||||
if (index === -1) return;
|
||||
|
||||
if (direction === 'up' && index > 0) {
|
||||
pushHistory();
|
||||
const newSections = [...sections];
|
||||
[newSections[index], newSections[index - 1]] = [newSections[index - 1], newSections[index]];
|
||||
set({ sections: newSections, hasUnsavedChanges: true });
|
||||
} else if (direction === 'down' && index < sections.length - 1) {
|
||||
pushHistory();
|
||||
const newSections = [...sections];
|
||||
[newSections[index], newSections[index + 1]] = [newSections[index + 1], newSections[index]];
|
||||
set({ sections: newSections, hasUnsavedChanges: true });
|
||||
}
|
||||
},
|
||||
|
||||
reorderSections: (sections) => {
|
||||
set({ sections, hasUnsavedChanges: true });
|
||||
reorderSections: (newSections) => {
|
||||
const { sections, pushHistory } = get();
|
||||
pushHistory();
|
||||
set({ sections: newSections, hasUnsavedChanges: true });
|
||||
},
|
||||
|
||||
updateSectionProp: (sectionId, propName, value) => {
|
||||
const { sections } = get();
|
||||
const { sections, pushHistory } = get();
|
||||
pushHistory();
|
||||
const newSections = sections.map(section => {
|
||||
if (section.id !== sectionId) return section;
|
||||
return {
|
||||
@@ -240,7 +323,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
},
|
||||
|
||||
updateSectionLayout: (sectionId, layoutVariant) => {
|
||||
const { sections } = get();
|
||||
const { sections, pushHistory } = get();
|
||||
pushHistory();
|
||||
const newSections = sections.map(section => {
|
||||
if (section.id !== sectionId) return section;
|
||||
return {
|
||||
@@ -252,7 +336,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
},
|
||||
|
||||
updateSectionColorScheme: (sectionId, colorScheme) => {
|
||||
const { sections } = get();
|
||||
const { sections, pushHistory } = get();
|
||||
pushHistory();
|
||||
const newSections = sections.map(section => {
|
||||
if (section.id !== sectionId) return section;
|
||||
return {
|
||||
@@ -264,7 +349,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
},
|
||||
|
||||
updateSectionStyles: (sectionId, styles) => {
|
||||
const { sections } = get();
|
||||
const { sections, pushHistory } = get();
|
||||
pushHistory();
|
||||
const newSections = sections.map(section => {
|
||||
if (section.id !== sectionId) return section;
|
||||
return {
|
||||
@@ -279,7 +365,8 @@ export const usePageEditorStore = create<PageEditorState>((set, get) => ({
|
||||
},
|
||||
|
||||
updateElementStyles: (sectionId, fieldName, styles) => {
|
||||
const { sections } = get();
|
||||
const { sections, pushHistory } = get();
|
||||
pushHistory();
|
||||
const newSections = sections.map(section => {
|
||||
if (section.id !== sectionId) return section;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user