import React, { useState, useEffect, useRef, useCallback } from 'react'; interface Project { title: string; description: string; link: string; category: string; tags?: string[]; order: number; } type ModalState = 'closed' | 'booting' | 'observe' | 'armed' | 'blocked'; interface ViewportPreset { name: string; width: number; height: number; label: string; } const VIEWPORT_PRESETS: ViewportPreset[] = [ { name: 'desktop', width: 1440, height: 900, label: 'Desktop' }, { name: 'tablet', width: 834, height: 1112, label: 'Tablet' }, { name: 'mobile', width: 390, height: 844, label: 'Mobile' }, ]; const MIN_BOOT_MS = 600; const IFRAME_TIMEOUT_MS = 4500; const DevEngageModal: React.FC = () => { const [modalState, setModalState] = useState('closed'); const [activeProject, setActiveProject] = useState(null); const [viewport, setViewport] = useState(VIEWPORT_PRESETS[0]); const [scale, setScale] = useState(1); const [bootProgress, setBootProgress] = useState(0); const [disarmToast, setDisarmToast] = useState(false); const [isDarkMode, setIsDarkMode] = useState(true); const iframeRef = useRef(null); const stageRef = useRef(null); const modalRef = useRef(null); const triggerRef = useRef(null); const bootStartRef = useRef(0); const iframeLoadedRef = useRef(false); const timeoutRef = useRef(null); const lockBodyScroll = useCallback(() => { const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; document.documentElement.style.overflow = 'hidden'; document.documentElement.style.paddingRight = `${scrollbarWidth}px`; }, []); const unlockBodyScroll = useCallback(() => { document.documentElement.style.overflow = ''; document.documentElement.style.paddingRight = ''; }, []); const calculateScale = useCallback(() => { if (!stageRef.current) return; const stage = stageRef.current.getBoundingClientRect(); const padding = 96; const availableW = stage.width - padding; const availableH = stage.height - padding; const scaleX = availableW / viewport.width; const scaleY = availableH / viewport.height; const fitScale = Math.min(scaleX, scaleY); setScale(Math.max(0.25, Math.min(fitScale, 1))); }, [viewport]); const openModal = useCallback((project: Project, triggerElement?: HTMLElement) => { if (triggerElement) triggerRef.current = triggerElement; setActiveProject(project); setModalState('booting'); setBootProgress(0); iframeLoadedRef.current = false; bootStartRef.current = performance.now(); lockBodyScroll(); }, [lockBodyScroll]); const closeModal = useCallback(() => { setModalState('closed'); setActiveProject(null); setDisarmToast(false); unlockBodyScroll(); if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } if (iframeRef.current) iframeRef.current.src = 'about:blank'; if (triggerRef.current) { triggerRef.current.focus(); triggerRef.current = null; } }, [unlockBodyScroll]); const handleBackdropClick = useCallback((e: React.MouseEvent) => { if (e.target === e.currentTarget) { if (modalState === 'armed') { setDisarmToast(true); setTimeout(() => setDisarmToast(false), 1500); } else if (modalState === 'observe' || modalState === 'blocked') { closeModal(); } } }, [modalState, closeModal]); const toggleArm = useCallback(() => { if (modalState === 'observe') setModalState('armed'); else if (modalState === 'armed') setModalState('observe'); }, [modalState]); const handleIframeLoad = useCallback(() => { iframeLoadedRef.current = true; }, []); const handleRetry = useCallback(() => { if (!activeProject) return; setModalState('booting'); setBootProgress(0); iframeLoadedRef.current = false; bootStartRef.current = performance.now(); if (iframeRef.current) iframeRef.current.src = activeProject.link; }, [activeProject]); useEffect(() => { const handleEngageEvent = (e: CustomEvent<{ project: Project; trigger?: HTMLElement }>) => { openModal(e.detail.project, e.detail.trigger); }; window.addEventListener('dev:engage' as any, handleEngageEvent); return () => window.removeEventListener('dev:engage' as any, handleEngageEvent); }, [openModal]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (modalState === 'closed') return; if (e.key === 'Escape') { e.preventDefault(); closeModal(); } if (modalState === 'observe' || modalState === 'armed') { if (e.key === '1') setViewport(VIEWPORT_PRESETS[0]); if (e.key === '2') setViewport(VIEWPORT_PRESETS[1]); if (e.key === '3') setViewport(VIEWPORT_PRESETS[2]); if (e.key === 'a' || e.key === 'A') toggleArm(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [modalState, closeModal, toggleArm]); useEffect(() => { if (modalState !== 'booting') return; const progressInterval = setInterval(() => { setBootProgress(p => Math.min(p + 3, 100)); }, 15); const checkBootCompletion = () => { const elapsed = performance.now() - bootStartRef.current; const minBootMet = elapsed >= MIN_BOOT_MS; const loaded = iframeLoadedRef.current; if (minBootMet && loaded) setModalState('observe'); else if (minBootMet && elapsed >= IFRAME_TIMEOUT_MS) setModalState('blocked'); else timeoutRef.current = window.setTimeout(checkBootCompletion, 100); }; timeoutRef.current = window.setTimeout(checkBootCompletion, 100); return () => { clearInterval(progressInterval); if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, [modalState]); useEffect(() => { if (modalState === 'closed') return; calculateScale(); const timer = setTimeout(calculateScale, 50); const handleResize = () => calculateScale(); window.addEventListener('resize', handleResize); return () => { clearTimeout(timer); window.removeEventListener('resize', handleResize); }; }, [modalState, viewport, calculateScale]); useEffect(() => { const checkDarkMode = () => { const theme = document.documentElement.getAttribute('data-theme'); setIsDarkMode(theme !== 'light'); }; checkDarkMode(); const observer = new MutationObserver(checkDarkMode); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); return () => observer.disconnect(); }, []); if (modalState === 'closed' || !activeProject) return null; const isInteractive = modalState === 'armed'; const showIframe = modalState !== 'booting'; return (
{/* Close button - positioned in preview area, not over sidebar */} {/* Main layout */}
{/* Preview area - takes most of the space, with left margin for site nav */}
{/* Iframe container */}