import React, { useState, useEffect, useCallback, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import axios from 'axios' import Pusher from 'pusher-js' import { Layers, Settings, Trash2, Plus, ChevronUp, ChevronDown, ArrowLeft, Loader2, Eye, Palette, Globe, FileEdit, FileText, ToggleLeft, ToggleRight, } from 'lucide-react' import BlockRenderer, { BLOCK_TYPES, BLOCK_TYPE_LABELS } from './BlockRenderer' import BlockStyleEditor from './BlockStyleEditor' import GlobalStylesPanel from './GlobalStylesPanel' const BASE_URL = import.meta.env.VITE_API_BASE_URL // ─── Sidebar tab definitions ──────────────────────────────────────────────── const TABS = [ { id: 'pages', icon: FileText, label: 'Pages' }, { id: 'blocks', icon: Layers, label: 'Blocks' }, { id: 'styles', icon: Palette, label: 'Styles' }, { id: 'lang', icon: Globe, label: 'Lang' }, ] export default function PageEditor() { const { pageId } = useParams() const navigate = useNavigate() const [page, setPage] = useState(null) const [blocks, setBlocks] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(null) const [selectedBlockId, setSelectedBlockId] = useState(null) const [activeTab, setActiveTab] = useState('blocks') const [leftPanel, setLeftPanel] = useState('blocks') // 'blocks' | 'styles' | 'content' | 'lang' const [toast, setToast] = useState(null) const [currentPageId, setCurrentPageId] = useState(null); const selectedBlock = blocks.find(b => b.id === selectedBlockId) || null const blockRefs = useRef({}) // ─── Load page ────────────────────────────────────────────────────────── useEffect(() => { fetchPage() }, [pageId]) // ─── Pusher live updates ───────────────────────────────────────────────── useEffect(() => { const pusher = new Pusher(import.meta.env.VITE_PUSHER_APP_KEY, { cluster: import.meta.env.VITE_PUSHER_CLUSTER || 'ap2', forceTLS: true, }) const channel = pusher.subscribe('page-builder-channel') channel.bind('page-updated', (data) => { if (data.page_slug === page?.slug) { setBlocks(data.blocks || []) showToast('Page updated in another session', 'info') } }) return () => { channel.unsubscribe(); pusher.disconnect() } }, [page?.slug]) const fetchPage = async () => { setLoading(true) let id = pageId || currentPageId console.log('[PE:fetchPage] triggered — pageId from URL:', pageId, '| currentPageId:', currentPageId, '| resolved id:', id) console.log('[PE:fetchPage] BASE_URL:', BASE_URL) try { if (!id || id === 'undefined') { console.log('[PE:fetchPage] no id found → fetching pages list for redirect') const res = await axios.get(`${BASE_URL}/admin/builder/pages`) console.log('[PE:fetchPage] pages list response status:', res.status, '| raw data:', res.data) const pages = res.data?.data ?? res.data ?? [] if (!pages.length) throw new Error('No pages found') const target = pages.find(p => p.slug === 'homepage') || pages[0] id = target.id console.log('[PE:fetchPage] redirecting to page:', id, target.slug) setCurrentPageId(id) navigate(`/admin/editor/${id}`, { replace: true }) return } console.log('[PE:fetchPage] fetching page id:', id, '| URL:', `${BASE_URL}/admin/builder/pages/${id}`) const res = await axios.get(`${BASE_URL}/admin/builder/pages/${id}`) console.log('[PE:fetchPage] response status:', res.status, '| raw data:', res.data) const data = res.data?.data ?? res.data console.log('[PE:fetchPage] resolved page:', data, '| blocks count:', data?.blocks?.length ?? 0) setPage(data) setCurrentPageId(data.id) setBlocks(data.blocks || []) } catch (err) { console.error('[PE:fetchPage] ERROR:', err.message) console.error('[PE:fetchPage] response status:', err.response?.status, '| body:', err.response?.data) showToast('Failed to load page', 'error') } finally { setLoading(false) console.log('[PE:fetchPage] done') } } // ─── Add block ─────────────────────────────────────────────────────────── const addBlock = async (blockType) => { const id = currentPageId console.log('[PE:addBlock] blockType:', blockType, '| currentPageId:', id) if (!id || id === 'undefined') { console.warn('[PE:addBlock] aborted — page not loaded yet') showToast('Page not loaded yet', 'error') return } try { const url = `${BASE_URL}/admin/builder/pages/${id}/blocks` console.log('[PE:addBlock] POST', url, { block_type: blockType }) const res = await axios.post(url, { block_type: blockType }) console.log('[PE:addBlock] response status:', res.status, '| new block:', res.data) const newBlock = res.data?.data ?? res.data setBlocks(prev => [...prev, newBlock]) showToast('Block added', 'success') } catch (err) { console.error('[PE:addBlock] ERROR:', err.message) console.error('[PE:addBlock] response status:', err.response?.status, '| body:', err.response?.data) showToast('Failed to add block', 'error') } } // ─── Save block styles ─────────────────────────────────────────────────── const saveBlockStyles = useCallback(async (blockId, styles) => { console.log('[PE:saveBlockStyles] blockId:', blockId, '| styles:', styles) setSaving(blockId) try { const url = `${BASE_URL}/admin/builder/blocks/${blockId}` console.log('[PE:saveBlockStyles] PUT', url) const res = await axios.put(url, { styles }) console.log('[PE:saveBlockStyles] saved — status:', res.status) setBlocks(prev => prev.map(b => b.id === blockId ? { ...b, styles } : b)) } catch (err) { console.error('[PE:saveBlockStyles] ERROR:', err.message) console.error('[PE:saveBlockStyles] response status:', err.response?.status, '| body:', err.response?.data) showToast('Failed to save styles', 'error') } finally { setSaving(null) } }, []) // ─── Save block content ────────────────────────────────────────────────── const saveBlockContent = useCallback(async (blockId, content) => { console.log('[PE:saveBlockContent] blockId:', blockId, '| content:', content) setSaving(blockId) try { const url = `${BASE_URL}/admin/builder/blocks/${blockId}` console.log('[PE:saveBlockContent] PUT', url) const res = await axios.put(url, { content }) console.log('[PE:saveBlockContent] saved — status:', res.status) setBlocks(prev => prev.map(b => b.id === blockId ? { ...b, content } : b)) } catch (err) { console.error('[PE:saveBlockContent] ERROR:', err.message) console.error('[PE:saveBlockContent] response status:', err.response?.status, '| body:', err.response?.data) showToast('Failed to save content', 'error') } finally { setSaving(null) } }, []) // ─── Delete block ──────────────────────────────────────────────────────── const deleteBlock = async (blockId) => { if (!window.confirm('Delete this block?')) return console.log('[PE:deleteBlock] deleting blockId:', blockId) try { const url = `${BASE_URL}/admin/builder/blocks/${blockId}` console.log('[PE:deleteBlock] DELETE', url) const res = await axios.delete(url) console.log('[PE:deleteBlock] deleted — status:', res.status) setBlocks(prev => prev.filter(b => b.id !== blockId)) if (selectedBlockId === blockId) setSelectedBlockId(null) showToast('Block deleted', 'success') } catch (err) { console.error('[PE:deleteBlock] ERROR:', err.message) console.error('[PE:deleteBlock] response status:', err.response?.status, '| body:', err.response?.data) showToast('Failed to delete block', 'error') } } // ─── Reorder block ─────────────────────────────────────────────────────── const moveBlock = async (index, direction) => { const newIndex = direction === 'up' ? index - 1 : index + 1 console.log('[PE:moveBlock] index:', index, '| direction:', direction, '| newIndex:', newIndex) if (newIndex < 0 || newIndex >= blocks.length) { console.log('[PE:moveBlock] out of bounds — aborting') return } const reordered = [...blocks] const [moved] = reordered.splice(index, 1) reordered.splice(newIndex, 0, moved) const withOrder = reordered.map((b, i) => ({ ...b, sort_order: i })) setBlocks(withOrder) console.log('[PE:moveBlock] new order:', withOrder.map(b => ({ id: b.id, sort_order: b.sort_order }))) try { const url = `${BASE_URL}/admin/builder/pages/${page?.id}/blocks/reorder` console.log('[PE:moveBlock] POST', url) const res = await axios.post(url, { order: withOrder.map(b => ({ id: b.id, sort_order: b.sort_order })), }) console.log('[PE:moveBlock] reordered — status:', res.status) } catch (err) { console.error('[PE:moveBlock] ERROR:', err.message) console.error('[PE:moveBlock] response status:', err.response?.status, '| body:', err.response?.data) showToast('Failed to reorder', 'error') fetchPage() } } // ─── Style / content change handlers ──────────────────────────────────── const handleStyleChange = (styles) => { if (!selectedBlockId) return console.log('[PE:handleStyleChange] blockId:', selectedBlockId, '| styles:', styles) setBlocks(prev => prev.map(b => b.id === selectedBlockId ? { ...b, styles } : b)) clearTimeout(window._styleTimer) window._styleTimer = setTimeout(() => saveBlockStyles(selectedBlockId, styles), 600) } const handleContentChange = (content) => { if (!selectedBlockId) return console.log('[PE:handleContentChange] blockId:', selectedBlockId, '| content:', content) setBlocks(prev => prev.map(b => b.id === selectedBlockId ? { ...b, content } : b)) clearTimeout(window._contentTimer) window._contentTimer = setTimeout(() => saveBlockContent(selectedBlockId, content), 600) } const showToast = (message, type = 'success') => { setToast({ message, type }) setTimeout(() => setToast(null), 3000) } // ─── Tab click ─────────────────────────────────────────────────────────── const handleTabClick = (tabId) => { setActiveTab(tabId) setLeftPanel(tabId) if (tabId !== 'content' && tabId !== 'block-styles') { setSelectedBlockId(null) } } // ─── Block selected — open content panel ───────────────────────────────── const selectBlock = (blockId) => { setSelectedBlockId(blockId) setLeftPanel('content') } // ─── Loading ────────────────────────────────────────────────────────────── if (loading) { return (
No blocks yet.
Click a block type in the left panel to add one.