Spaces:
Running
Running
| import { useState, useRef, useCallback, useEffect } from 'react'; | |
| import { Play, Square, RefreshCw, ExternalLink, X } from 'lucide-react'; | |
| interface ScriptPreviewProps { | |
| htmlContent: string; | |
| scriptContent: string; | |
| fileName: string; | |
| onClose: () => void; | |
| } | |
| const NAV_INTERCEPTOR = ` | |
| <script> | |
| (function() { | |
| var origin = window.location.origin; | |
| function intercept(url, target) { | |
| window.parent.postMessage({ type: 'preview-navigate', url: url, target: target }, origin); | |
| } | |
| var origOpen = window.open; | |
| window.open = function(url, name, features) { | |
| intercept(url, '_blank'); | |
| return null; | |
| }; | |
| document.addEventListener('click', function(e) { | |
| var a = e.target.closest('a'); | |
| if (a && a.href && (a.target === '_blank' || a.getAttribute('rel') === 'noopener')) { | |
| e.preventDefault(); | |
| intercept(a.href, '_blank'); | |
| } | |
| }); | |
| })(); | |
| </script> | |
| `; | |
| export default function ScriptPreview({ htmlContent, scriptContent, fileName, onClose }: ScriptPreviewProps) { | |
| const [running, setRunning] = useState(false); | |
| const [key, setKey] = useState(0); | |
| const [pendingUrl, setPendingUrl] = useState<string | null>(null); | |
| const iframeRef = useRef<HTMLIFrameElement>(null); | |
| // Listen for navigation events from the iframe | |
| useEffect(() => { | |
| function handler(e: MessageEvent) { | |
| if (e.data?.type === 'preview-navigate' && e.data.url) { | |
| setPendingUrl(e.data.url); | |
| } | |
| } | |
| window.addEventListener('message', handler); | |
| return () => window.removeEventListener('message', handler); | |
| }, []); | |
| const buildFullHtml = useCallback(() => { | |
| let fullHtml = htmlContent; | |
| if (scriptContent) { | |
| const scriptTag = `<script>${scriptContent}\n</script>`; | |
| if (fullHtml.includes('</body>')) { | |
| fullHtml = fullHtml.replace('</body>', `${NAV_INTERCEPTOR}\n${scriptTag}\n</body>`); | |
| } else { | |
| fullHtml += `\n${NAV_INTERCEPTOR}\n${scriptTag}`; | |
| } | |
| } else if (fullHtml.includes('</body>')) { | |
| fullHtml = fullHtml.replace('</body>', `${NAV_INTERCEPTOR}\n</body>`); | |
| } else { | |
| fullHtml += `\n${NAV_INTERCEPTOR}`; | |
| } | |
| return fullHtml; | |
| }, [htmlContent, scriptContent]); | |
| const handleStart = useCallback(() => { | |
| setKey((k) => k + 1); | |
| setRunning(true); | |
| }, []); | |
| const handleStop = useCallback(() => { | |
| setRunning(false); | |
| setKey((k) => k + 1); | |
| }, []); | |
| const handleRestart = useCallback(() => { | |
| handleStop(); | |
| setTimeout(() => handleStart(), 50); | |
| }, [handleStart, handleStop]); | |
| const confirmRedirect = useCallback(() => { | |
| if (pendingUrl) { | |
| window.open(pendingUrl, '_blank', 'noopener,noreferrer'); | |
| } | |
| setPendingUrl(null); | |
| }, [pendingUrl]); | |
| const cancelRedirect = useCallback(() => { | |
| setPendingUrl(null); | |
| }, []); | |
| const fullHtml = buildFullHtml(); | |
| return ( | |
| <div className="flex flex-col h-full bg-surface-950"> | |
| {/* Preview Toolbar */} | |
| <div className="flex items-center gap-2 px-4 py-2 bg-surface-900 border-b border-surface-700"> | |
| <div className="flex items-center gap-1.5 text-xs text-surface-300"> | |
| <ExternalLink className="w-3.5 h-3.5 text-primary-400" /> | |
| <span className="font-medium">Preview:</span> | |
| <span className="text-surface-400">{fileName}</span> | |
| </div> | |
| <div className="flex-1" /> | |
| {!running ? ( | |
| <button onClick={handleStart} | |
| className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-green-600 text-white hover:bg-green-500 transition-colors"> | |
| <Play className="w-3.5 h-3.5" /> Run | |
| </button> | |
| ) : ( | |
| <> | |
| <button onClick={handleRestart} | |
| className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-surface-700 text-surface-200 hover:bg-surface-600 transition-colors"> | |
| <RefreshCw className="w-3.5 h-3.5" /> Restart | |
| </button> | |
| <button onClick={handleStop} | |
| className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-red-600 text-white hover:bg-red-500 transition-colors"> | |
| <Square className="w-3.5 h-3.5" /> Stop | |
| </button> | |
| </> | |
| )} | |
| <button onClick={onClose} | |
| className="p-1.5 text-surface-400 hover:text-white rounded-md hover:bg-surface-800 transition-colors"> | |
| <X className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| {/* Preview Area */} | |
| <div className="flex-1 bg-white"> | |
| {running ? ( | |
| <iframe | |
| key={key} | |
| ref={iframeRef} | |
| srcDoc={fullHtml} | |
| className="w-full h-full border-0" | |
| sandbox="allow-scripts allow-forms" | |
| title="Script Preview" | |
| /> | |
| ) : ( | |
| <div className="flex items-center justify-center h-full text-surface-500 text-sm"> | |
| <div className="text-center"> | |
| <Play className="w-10 h-10 mx-auto mb-3 opacity-30" /> | |
| <p>Click <span className="text-green-400 font-medium">Run</span> to start the script</p> | |
| <p className="text-xs mt-1 text-surface-600">Stopping and restarting always starts fresh</p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Redirect confirmation modal */} | |
| {pendingUrl && ( | |
| <div className="absolute inset-0 flex items-center justify-center bg-black/50 z-50"> | |
| <div className="bg-surface-900 rounded-xl shadow-2xl border border-surface-700 p-6 max-w-md mx-4"> | |
| <h3 className="text-lg font-semibold text-white mb-2">External Redirect</h3> | |
| <p className="text-surface-300 text-sm mb-4"> | |
| This preview is trying to navigate you to: | |
| </p> | |
| <div className="bg-surface-800 rounded-lg px-3 py-2 mb-4 text-sm text-primary-300 break-all font-mono"> | |
| {pendingUrl} | |
| </div> | |
| <div className="flex gap-2 justify-end"> | |
| <button onClick={cancelRedirect} | |
| className="px-4 py-2 text-sm rounded-lg bg-surface-700 text-surface-200 hover:bg-surface-600 transition-colors"> | |
| Block | |
| </button> | |
| <button onClick={confirmRedirect} | |
| className="px-4 py-2 text-sm rounded-lg bg-primary-600 text-white hover:bg-primary-500 transition-colors"> | |
| Open in New Tab | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |