import React, { useCallback, useState, useEffect, useRef } from 'react'; import { useEditor } from '@craftjs/core'; import { useEditorConfig } from '../../state/EditorConfigContext'; import { useWhpApi } from '../../hooks/useWhpApi'; import { usePages } from '../../state/PageContext'; import { useSiteDesign } from '../../state/SiteDesignContext'; import { useIsMobile } from '../../hooks/useIsMobile'; import { useMobileChrome } from '../../state/MobileChromeContext'; import { DeviceMode } from '../../types'; import { TemplateModal } from './TemplateModal'; import { HeadCodeModal } from './HeadCodeModal'; import { ReportIssueModal } from './ReportIssueModal'; import { TopBarOverflowMenu } from './TopBarOverflowMenu'; import { PublishWarnings } from './PublishWarnings'; import { SitesmithButton } from '../sitesmith/SitesmithButton'; import { useSitesmithModal } from '../../state/SitesmithContext'; interface TopBarProps { device: DeviceMode; onDeviceChange: (device: DeviceMode) => void; /** Item 10: canvas dashed guide outlines, default ON. */ showGuides: boolean; onToggleGuides: () => void; } export const TopBar: React.FC = ({ device, onDeviceChange, showGuides, onToggleGuides }) => { const { whpConfig, isWHP } = useEditorConfig(); const { actions, query, canUndo, canRedo } = useEditor((_state, query) => ({ canUndo: query.history.canUndo(), canRedo: query.history.canRedo(), })); const { save, publish, load } = useWhpApi(); const { headerPage, footerPage, pages, activePageId } = usePages(); const { design } = useSiteDesign(); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); const [publishStatus, setPublishStatus] = useState<'idle' | 'publishing' | 'published' | 'error'>('idle'); const [publishWarnings, setPublishWarnings] = useState([]); const [isDraft, setIsDraft] = useState(false); // Mobile-A2: lifted from private useState into MobileChromeContext so // opening a mobile sheet can close these modals (item 3) -- behavior is // otherwise identical for both the desktop and mobile branches below. const { templateModalOpen, setTemplateModalOpen, headCodeModalOpen, setHeadCodeModalOpen, overflowOpen, setOverflowOpen } = useMobileChrome(); const [reportOpen, setReportOpen] = useState(false); const isMobile = useIsMobile(); const { open: openSitesmith } = useSitesmithModal(); const saveTimeoutRef = useRef | null>(null); const publishTimeoutRef = useRef | null>(null); const hasLoadedRef = useRef(false); // Load saved state on mount useEffect(() => { if (!isWHP || hasLoadedRef.current) return; hasLoadedRef.current = true; load().catch((e) => { console.warn('Failed to load project from WHP API:', e); }); }, [isWHP, load]); // Auto-save every 30 seconds useEffect(() => { if (!isWHP) return; const interval = setInterval(() => { save() .then((result) => { if (result?.success) { setSaveStatus('saved'); setIsDraft(true); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(() => setSaveStatus('idle'), 2000); } }) .catch(() => { // Silent fail for auto-save }); }, 30000); return () => clearInterval(interval); }, [isWHP, save]); const handleSave = useCallback(async () => { setSaveStatus('saving'); try { const result = await save(); if (result?.success) { setSaveStatus('saved'); setIsDraft(true); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(() => setSaveStatus('idle'), 2500); } else { setSaveStatus('error'); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(() => setSaveStatus('idle'), 3000); } } catch (e) { console.error('Save failed:', e); setSaveStatus('error'); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(() => setSaveStatus('idle'), 3000); } }, [save]); const handlePublish = useCallback(async () => { setPublishStatus('publishing'); setPublishWarnings([]); try { const result = await publish(); if (result?.success) { setPublishStatus('published'); setIsDraft(false); // The site published; these are fixable problems, not failures. Held // independently of publishStatus so the 3s "Published" flash doesn't // take the warning down with it. setPublishWarnings(Array.isArray(result.warnings) ? result.warnings : []); if (publishTimeoutRef.current) clearTimeout(publishTimeoutRef.current); publishTimeoutRef.current = setTimeout(() => setPublishStatus('idle'), 3000); } else { setPublishStatus('error'); if (publishTimeoutRef.current) clearTimeout(publishTimeoutRef.current); publishTimeoutRef.current = setTimeout(() => setPublishStatus('idle'), 3000); } } catch (e) { console.error('Publish failed:', e); setPublishStatus('error'); if (publishTimeoutRef.current) clearTimeout(publishTimeoutRef.current); publishTimeoutRef.current = setTimeout(() => setPublishStatus('idle'), 3000); } }, [publish]); // Cleanup timeouts on unmount useEffect(() => { return () => { if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); if (publishTimeoutRef.current) clearTimeout(publishTimeoutRef.current); }; }, []); // Extracted so both the desktop inline Preview button and the mobile // overflow-menu "Preview" item can trigger the exact same flow. const handlePreview = useCallback(() => { try { const serialized = query.serialize(); import('../../utils/html-export').then(({ exportToHtml, exportBodyHtml, buildAnimationScript }) => { // Get header HTML let headerHtml = ''; try { if (headerPage.craftState) { headerHtml = exportBodyHtml(headerPage.craftState).html; } } catch (e) { console.warn('Header export failed:', e); } // Get page body HTML const bodyResult = exportBodyHtml(serialized); const bodyHtml = bodyResult.html; // Get footer HTML let footerHtml = ''; try { if (footerPage.craftState) { footerHtml = exportBodyHtml(footerPage.craftState).html; } } catch (e) { console.warn('Footer export failed:', e); } // Compose full page: header + body + footer. `handlePreview` below // replaces the ENTIRE wrapped-doc `` inner (including the // in-body reveal `