import { useEffect, useRef, useState } from 'react' import grapesjs from 'grapesjs' import 'grapesjs/dist/css/grapes.min.css' import newsletter from 'grapesjs-preset-newsletter' import { Plus, Palette, Monitor, Smartphone, Tablet, Layers, Image as ImageIcon, Save, Loader2, Undo2, Redo2, Maximize, Eye, Trash2, PanelLeftClose, PanelLeftOpen, ArrowLeft, Search, Layout, Mail, Code } from 'lucide-react' import { GhostButton, AdminButton, SecondaryButton, SettingInput } from '../ui/settings-ui' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' import { registerPremiumBlocks } from './EditorBlocks' interface LocalBlock { get: (key: string) => unknown; } interface GrapesEditorProps { initialContentJson?: string; initialContentHtml?: string; onSave: (json: string, html: string) => void; saving?: boolean; height?: string; templateName?: string; onNameChange?: (name: string) => void; showNameInput?: boolean; onBack?: () => void; onSendTest?: (html: string, email: string) => void; sendingTest?: boolean; } export default function GrapesEditor({ initialContentJson, initialContentHtml, onSave, saving = false, height = 'calc(100vh - 180px)', templateName: initialTemplateName = '', onNameChange, showNameInput = false, onBack, onSendTest, sendingTest = false }: GrapesEditorProps) { const [editor, setEditor] = useState(null) const gjsContainerRef = useRef(null) const mainWrapperRef = useRef(null) const [activePanel, setActivePanel] = useState('blocks') const [currentDevice, setCurrentDevice] = useState('Desktop') const [sidebarOpen, setSidebarOpen] = useState(true) const [searchTerm, setSearchTerm] = useState('') const [templateName, setTemplateName] = useState(initialTemplateName) const [testEmail, setTestEmail] = useState('') const [blockCounts, setBlockCounts] = useState({ elements: 0, templates: 0 }) useEffect(() => { if (!gjsContainerRef.current) return const gjs = grapesjs.init({ container: gjsContainerRef.current, fromElement: false, height: '100%', width: 'auto', storageManager: false, plugins: [newsletter], pluginsOpts: { [newsletter as unknown as string]: {} }, blockManager: { appendTo: '#gjs-blocks' }, layerManager: { appendTo: '#gjs-layers' }, selectorManager: { appendTo: '#gjs-styles' }, styleManager: { appendTo: '#gjs-styles' }, traitManager: { appendTo: '#gjs-styles' }, panels: { defaults: [] }, deviceManager: { devices: [ { name: 'Desktop', width: '' }, { name: 'Tablet', width: '768px', widthMedia: '768px' }, { name: 'Mobile', width: '320px', widthMedia: '480px' } ] }, canvas: { styles: [ 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap' ] }, log: [] }) registerPremiumBlocks(gjs) if (initialContentJson) { try { const projectData = JSON.parse(initialContentJson); if (projectData && (projectData.pages || projectData.styles || projectData.assets)) { gjs.loadProjectData(projectData); } else { gjs.setComponents(projectData); } } catch { console.error('Failed to parse initial JSON'); } } else if (initialContentHtml) { gjs.setComponents(initialContentHtml); } setEditor(gjs) gjs.on('component:selected', () => { setSidebarOpen(true) }) return () => { if (gjs) gjs.destroy() } }, [initialContentJson, initialContentHtml, onSave]); useEffect(() => { if (!editor) return; const bm = editor.BlockManager; const allBlocks = bm.getAll(); const isBlocksPanel = activePanel === 'blocks'; const isTemplatesPanel = activePanel === 'templates'; // Category labels for "Templates" const templateCats = ['Top Bars', 'Elite Headers', 'Conversion Hero', 'Welcome & Greet', 'WhatsApp & Social', 'Articles & Blog', 'WooCommerce Hub', 'Cart Recovery', 'Marketing Assets', 'Coupons & Gifts', 'Auth & Security', 'Signature & Info', 'Quick Support', 'Pricing Plans', 'Trust & Proof', 'Elite Footers']; const filtered = allBlocks.filter((item: unknown) => { const block = item as LocalBlock; const category = block.get('category'); const catLabel = typeof category === 'string' ? category : (String((category as LocalBlock)?.get?.('label') || '') || (category as { id?: string })?.id || ''); const isTemplate = templateCats.includes(String(catLabel)); if (isBlocksPanel && isTemplate) return false; if (isTemplatesPanel && !isTemplate) return false; if (!isBlocksPanel && !isTemplatesPanel) return false; // Search logic if (searchTerm.trim()) { const name = String(block.get('name') || '').toLowerCase(); const label = String(block.get('label') || '').toLowerCase(); const search = searchTerm.toLowerCase(); // Check if item has a label that's not just an SVG const hasText = !label.startsWith(' { const block = item as LocalBlock; const category = block.get('category'); const catLabel = typeof category === 'string' ? category : (String((category as LocalBlock)?.get?.('label') || '') || (category as { id?: string })?.id || ''); return templateCats.includes(String(catLabel)); }).length; setTimeout(() => { setBlockCounts({ templates: templateCounts, elements: allBlocks.length - templateCounts }); }, 0); bm.render(filtered); }, [searchTerm, editor, activePanel]); const handleSave = () => { if (!editor) return; // Force inlining by getting HTML (which is now inlined due to inlineCss: true) const html = editor.getHtml(); const css = editor.getCss(); const fullHtml = `${html}`; const json = JSON.stringify(editor.getProjectData()); onSave(json, fullHtml); } const handleDeviceChange = (device: string) => { if (!editor) return setCurrentDevice(device) editor.setDevice(device) } const handlePanelChange = (panel: string) => { setActivePanel(panel) if (!editor) return if (panel === 'images') editor.runCommand('open-assets') } const handleUndo = () => editor?.UndoManager.undo() const handleRedo = () => editor?.UndoManager.redo() const handleClear = () => { if (confirm('Are you sure you want to clear the canvas?')) { editor?.setComponents('') } } const handleViewCode = () => { if (!editor) return; editor.runCommand('export-template'); } const toggleFullscreen = () => { if (!mainWrapperRef.current) return const el = mainWrapperRef.current if (!document.fullscreenElement) { el.requestFullscreen().catch(err => { alert(`Error escaping fullscreen mode: ${err.message}`) }) } else { document.exitFullscreen() } } return (
{/* Toolbar */}
setSidebarOpen(!sidebarOpen)} className="text-slate-500 hover:text-[#22c55e] hover:bg-slate-50"> {sidebarOpen ? : }

{sidebarOpen ? 'Collapse Sidebar' : 'Expand Sidebar'}

Undo

Redo

Fullscreen

editor?.runCommand('preview')} className="text-slate-400 hover:text-[#22c55e] hover:bg-slate-50">

Toggle Preview

View Code

Clear All

handleDeviceChange('Desktop')} className={`rounded-[10px] w-9 h-9 ${currentDevice === 'Desktop' ? 'bg-white text-[#22c55e] shadow-sm' : 'text-slate-400 hover:text-slate-600 hover:bg-white/50'}`}>

Desktop

handleDeviceChange('Tablet')} className={`rounded-[10px] w-9 h-9 ${currentDevice === 'Tablet' ? 'bg-white text-[#22c55e] shadow-sm' : 'text-slate-400 hover:text-slate-600 hover:bg-white/50'}`}>

Tablet

handleDeviceChange('Mobile')} className={`rounded-[10px] w-9 h-9 ${currentDevice === 'Mobile' ? 'bg-white text-[#22c55e] shadow-sm' : 'text-slate-400 hover:text-slate-600 hover:bg-white/50'}`}>

Mobile

{showNameInput && ( { setTemplateName(val); onNameChange?.(val); }} placeholder="Name your template..." className="w-64 max-w-[250px]" labelHidden /> )} {saving ? : } Save Template
{ if (!editor) return; const html = editor.getHtml(); const css = editor.getCss(); const fullHtml = `${html}`; onSendTest?.(fullHtml, testEmail); }} disabled={sendingTest || !testEmail} className="h-10 px-4 font-semibold border-blue-200 text-blue-600 hover:bg-blue-50 transition-all active:scale-95" > {sendingTest ? : } Send Test
handlePanelChange('blocks')}> {blockCounts.elements > 0 && ( {blockCounts.elements} )}

Elements ({blockCounts.elements})

handlePanelChange('templates')}> {blockCounts.templates > 0 && ( {blockCounts.templates} )}

Templates ({blockCounts.templates})

handlePanelChange('layers')}>

Layers

handlePanelChange('styles')}>

Styles

handlePanelChange('images')}>

Images

{onBack && ( <>

Back to List

)}
{/* ── Sidebar panel ───────────────────────────────── */}
{/* Fixed search header */} {(activePanel === 'blocks' || activePanel === 'templates') && (
{activePanel === 'templates' ? 'Templates Library' : 'Basic Elements'} {activePanel === 'templates' ? blockCounts.templates : blockCounts.elements} Blocks
)} {/* position:relative wrapper → children use position:absolute. This breaks out of the document flow so blocks can NEVER push the page height, yet scroll freely inside. */}
) }