From f4f7ff10f017fee143a74d1afe86fe37529e83fe Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Mon, 12 Jan 2026 12:08:03 +0700 Subject: [PATCH] feat: Page Editor live preview - Add POST /preview/page/{slug} and /preview/template/{cpt} endpoints - Render full HTML using PageSSR for iframe preview - Templates use sample post for dynamic placeholder resolution - PageSettings iframe with debounced section updates (500ms) - Desktop/Mobile toggle with scaled iframe view - Show/Hide preview toggle button - Refresh button for manual preview reload - Preview indicator banner in iframe --- .../Pages/components/PageSettings.tsx | 159 ++++++++++++- .../src/routes/Appearance/Pages/index.tsx | 1 + includes/Api/PagesController.php | 214 ++++++++++++++++++ 3 files changed, 363 insertions(+), 11 deletions(-) diff --git a/admin-spa/src/routes/Appearance/Pages/components/PageSettings.tsx b/admin-spa/src/routes/Appearance/Pages/components/PageSettings.tsx index eb76fad..32292e9 100644 --- a/admin-spa/src/routes/Appearance/Pages/components/PageSettings.tsx +++ b/admin-spa/src/routes/Appearance/Pages/components/PageSettings.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; +import { api } from '@/lib/api'; import { __ } from '@/lib/i18n'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; @@ -13,7 +14,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; -import { Settings, Eye, Smartphone, Monitor, ExternalLink } from 'lucide-react'; +import { Settings, Eye, Smartphone, Monitor, ExternalLink, RefreshCw, Loader2 } from 'lucide-react'; interface Section { id: string; @@ -40,6 +41,7 @@ interface AvailableSource { interface PageSettingsProps { page: PageItem | null; section: Section | null; + sections: Section[]; // All sections for preview onSectionUpdate: (section: Section) => void; isTemplate?: boolean; availableSources?: AvailableSource[]; @@ -111,11 +113,84 @@ const COLOR_SCHEMES = [ export function PageSettings({ page, section, + sections, onSectionUpdate, isTemplate = false, availableSources = [], }: PageSettingsProps) { - const [previewMode, setPreviewMode] = React.useState<'desktop' | 'mobile'>('desktop'); + const [previewMode, setPreviewMode] = useState<'desktop' | 'mobile'>('desktop'); + const [previewHtml, setPreviewHtml] = useState(null); + const [previewLoading, setPreviewLoading] = useState(false); + const [showPreview, setShowPreview] = useState(false); + const iframeRef = useRef(null); + const previewTimeoutRef = useRef(null); + + // Debounced preview fetch + useEffect(() => { + if (!page || !showPreview) return; + + // Clear existing timeout + if (previewTimeoutRef.current) { + clearTimeout(previewTimeoutRef.current); + } + + // Debounce preview updates + previewTimeoutRef.current = setTimeout(async () => { + setPreviewLoading(true); + try { + const endpoint = page.type === 'page' + ? `/preview/page/${page.slug}` + : `/preview/template/${page.cpt}`; + + const response = await api.post(endpoint, { sections }); + if (response?.html) { + setPreviewHtml(response.html); + } + } catch (error) { + console.error('Preview error:', error); + } finally { + setPreviewLoading(false); + } + }, 500); + + return () => { + if (previewTimeoutRef.current) { + clearTimeout(previewTimeoutRef.current); + } + }; + }, [page, sections, showPreview]); + + // Update iframe when HTML changes + useEffect(() => { + if (iframeRef.current && previewHtml) { + const doc = iframeRef.current.contentDocument; + if (doc) { + doc.open(); + doc.write(previewHtml); + doc.close(); + } + } + }, [previewHtml]); + + // Manual refresh + const handleRefreshPreview = async () => { + if (!page) return; + setPreviewLoading(true); + try { + const endpoint = page.type === 'page' + ? `/preview/page/${page.slug}` + : `/preview/template/${page.cpt}`; + + const response = await api.post(endpoint, { sections }); + if (response?.html) { + setPreviewHtml(response.html); + } + } catch (error) { + console.error('Preview error:', error); + } finally { + setPreviewLoading(false); + } + }; // Update section prop const updateProp = (name: string, value: any, isDynamic?: boolean) => { @@ -310,16 +385,30 @@ export function PageSettings({ )} - {/* Preview Toggle */} + {/* Preview Panel */} - - - {__('Preview')} + + + + {__('Preview')} + + {showPreview && ( + + )} -
+ {/* Preview Mode Toggle */} +
-

- {__('Live preview will be available after saving.')} -

+ + {/* Preview Toggle */} + {!showPreview ? ( + + ) : ( +
+ {/* Preview Iframe Container */} +
+ {previewLoading && ( +
+ +
+ )} +