import { useState, useRef, useEffect, useCallback } from 'react'; import { Cpu, Play, ExternalLink, AlertCircle } from 'lucide-react'; import type { SimulationRef } from '@/lib/types'; interface SimulationEmbedProps { simulations: SimulationRef[]; spicebookUrl: string; } interface SimCardProps { sim: SimulationRef; spicebookUrl: string; } function SimCard({ sim, spicebookUrl }: SimCardProps) { const [active, setActive] = useState(false); const [loaded, setLoaded] = useState(false); const [error, setError] = useState(false); const iframeRef = useRef(null); const timeoutRef = useRef>(); const loadedRef = useRef(false); const embedUrl = `${spicebookUrl}/embed/${sim.notebookId}`; // Detect current theme from class const getTheme = useCallback(() => { return document.documentElement.classList.contains('dark') ? 'dark' : 'light'; }, []); // Send theme to iframe via postMessage const sendTheme = useCallback((theme: string) => { if (iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.postMessage( { type: 'spicebook-theme', theme }, spicebookUrl ); } }, [spicebookUrl]); // Watch for theme changes on element useEffect(() => { if (!active) return; const observer = new MutationObserver(() => { sendTheme(getTheme()); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'], }); return () => observer.disconnect(); }, [active, getTheme, sendTheme]); // Keep loadedRef in sync so the timeout can check current state // without capturing a stale closure value useEffect(() => { loadedRef.current = loaded; }, [loaded]); // Set a load timeout — if the iframe doesn't fire onload within 8s, // assume SpiceBook is unreachable useEffect(() => { if (!active) return; timeoutRef.current = setTimeout(() => { if (!loadedRef.current) setError(true); }, 8000); return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, [active]); // Stop in-flight network requests when unmounting the iframe useEffect(() => { return () => { if (iframeRef.current) { iframeRef.current.src = 'about:blank'; } }; }, []); const handleLoad = () => { setLoaded(true); setError(false); if (timeoutRef.current) clearTimeout(timeoutRef.current); sendTheme(getTheme()); }; const handleActivate = () => { setActive(true); setError(false); setLoaded(false); }; if (!active) { // Preview card — click to activate return (

{sim.title}

{sim.description}

{sim.pageRef && ( {sim.pageRef} )}
{/* Decorative circuit trace along bottom */}
); } // Active state — iframe loaded (or loading/error) return (
{/* Header bar */}
{sim.title} {sim.pageRef && ( ({sim.pageRef}) )}
{/* Iframe container with 16:10 aspect ratio */}
{/* Loading state */} {!loaded && !error && (
Loading simulation...
)} {/* Error/fallback state */} {error && (

Simulation coming soon

This notebook is being prepared.{' '} Try SpiceBook directly

)} {/* SECURITY: allow-same-origin is required so SpiceBook can access its own storage/cookies. Safe because the iframe src is a different origin. The parent MUST validate origin on any future postMessage listener. NOTE: iframe onError does NOT fire for HTTP 4xx/5xx responses — the 8s timeout is the primary error detection mechanism. A postMessage handshake with SpiceBook would provide reliable HTTP error detection. */}