diff --git a/src/components/dev/DevEngageModal.tsx b/src/components/dev/DevEngageModal.tsx new file mode 100644 index 0000000..12c3c7a --- /dev/null +++ b/src/components/dev/DevEngageModal.tsx @@ -0,0 +1,546 @@ +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 = 850; +const IFRAME_TIMEOUT_MS = 4500; + +const BOOT_LOG_LINES = [ + 'INIT: FRAMEBUFFER', + 'UPLINK: ESTABLISHING', + 'AUTH: INPUT_LOCKED', + 'SIGNAL: STABLE', +]; + +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 [bootLogIndex, setBootLogIndex] = useState(0); + const [disarmToast, setDisarmToast] = useState(false); + + 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 stagePadding = 80; + const availableW = stage.width - stagePadding; + const availableH = stage.height - stagePadding; + const scaleX = availableW / viewport.width; + const scaleY = availableH / viewport.height; + setScale(Math.min(scaleX, scaleY, 1)); + }, [viewport]); + + const openModal = useCallback((project: Project, triggerElement?: HTMLElement) => { + if (triggerElement) { + triggerRef.current = triggerElement; + } + setActiveProject(project); + setModalState('booting'); + setBootProgress(0); + setBootLogIndex(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); + setBootLogIndex(0); + iframeLoadedRef.current = false; + bootStartRef.current = performance.now(); + if (iframeRef.current) { + iframeRef.current.src = activeProject.link; + } + }, [activeProject]); + + const handleCopyLink = useCallback(async () => { + if (!activeProject) return; + try { + await navigator.clipboard.writeText(activeProject.link); + } catch { + const input = document.createElement('input'); + input.value = activeProject.link; + document.body.appendChild(input); + input.select(); + document.execCommand('copy'); + document.body.removeChild(input); + } + }, [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 + 2, 100)); + }, 15); + + const logInterval = setInterval(() => { + setBootLogIndex(i => Math.min(i + 1, BOOT_LOG_LINES.length)); + }, 180); + + 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); + clearInterval(logInterval); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [modalState]); + + useEffect(() => { + if (modalState === 'closed') return; + calculateScale(); + + const handleResize = () => calculateScale(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [modalState, viewport, calculateScale]); + + if (modalState === 'closed' || !activeProject) return null; + + const isInteractive = modalState === 'armed'; + const showIframe = modalState !== 'booting'; + + return ( +
+
+ +
+ +
+ +
+ +
+
+
+
+ + SYS.DEV /// LIVE_FEED + +
+ + PRJ.0{activeProject.order} / {activeProject.category} + +
+ +
+
+ + MODE: + {modalState === 'booting' ? 'BOOT' : modalState === 'blocked' ? 'ERROR' : modalState.toUpperCase()} + + + + INPUT: + {isInteractive ? 'ARMED' : 'LOCKED'} + + +
+ + +
+
+ +
+ +
+ +
+
+
+
+
+ +
-
-
- -
-
- Live Connection +
+
+ + + + Click to Engage
@@ -131,7 +144,6 @@ const pageTitle = `Dev | ${SITE_TITLE}`;
-
@@ -159,4 +171,49 @@ const pageTitle = `Dev | ${SITE_TITLE}`;
- \ No newline at end of file + + + + +