RealBlocks / client /src /components /ProjectEditor /ScriptPreview.tsx
incognitolm
Update ScriptPreview.tsx
8c2fa66
Raw
History Blame Contribute Delete
6.5 kB
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>
);
}