356 lines · 10.5 KB · TypeScript
RawDownload
'use client'; import { useState, useRef, useEffect, useCallback } from 'react';import {  Play,  Square,  Copy,  Check,  Download,  Maximize2,  Minimize2,  RefreshCw,  AlertTriangle,} from 'lucide-react'; interface CodeSandboxProps {  code: string;  language: 'html' | 'react' | 'javascript';  title?: string;  autoRun?: boolean;  onOutput?: (output: string) => void;  onError?: (error: string) => void;} const transformReactCode = (code: string) => {  let transformed = code || '';  // Strip common import/export patterns that won't work in a plain iframe without bundling.  transformed = transformed.replace(/^\s*import\s+.*?;?\s*$/gm, '');  transformed = transformed.replace(/^\s*export\s+\{[^}]+\}\s*;?\s*$/gm, '');  transformed = transformed.replace(/^\s*export\s+(const|let|var|function|class)\s+/gm, '$1 ');  // Capture default export into a known global.  transformed = transformed.replace(/\bexport\s+default\s+/g, 'window.__DEFAULT_EXPORT__ = ');  return transformed.trim();}; // Template for React executionconst REACT_TEMPLATE = (code: string) => `<!DOCTYPE html><html><head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>  <style>    * { margin: 0; padding: 0; box-sizing: border-box; }    body { font-family: system-ui, -apple-system, sans-serif; padding: 16px; }    .error { color: #b91c1c; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; white-space: pre-wrap; }  </style></head><body>  <div id="root"></div>  <div id="error" class="error" style="display:none;"></div>  <script type="text/babel" data-presets="react,typescript">    (() => {      const send = (type, message) => {        try { window.parent && window.parent.postMessage({ type, message }, '*'); } catch {}      };      const showError = (message) => {        const el = document.getElementById('error');        if (el) {          el.style.display = 'block';          el.textContent = message;        }      };       window.addEventListener('error', (event) => {        const msg = event?.error?.stack || event?.message || 'Unknown error';        send('error', msg);        showError(msg);      });       try {        window.__DEFAULT_EXPORT__ = undefined;         const { useState, useEffect, useMemo, useRef, useCallback } = React;         ${transformReactCode(code)}         // Try to render common component patterns        const container = document.getElementById('root');        const root = ReactDOM.createRoot(container);         const candidate =          (typeof App !== 'undefined' && App) ||          window.__DEFAULT_EXPORT__ ||          null;         if (!candidate) {          const msg = 'React preview: define an App component or use export default.';          send('error', msg);          showError(msg);          return;        }         const element = React.isValidElement(candidate)          ? candidate          : React.createElement(candidate);         root.render(element);      } catch (e) {        const msg = e?.stack || e?.message || 'Failed to render React preview';        send('error', msg);        showError(msg);      }    })();  </script></body></html>`; // Template for vanilla JavaScriptconst JS_TEMPLATE = (code: string) => `<!DOCTYPE html><html><head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <style>    * { margin: 0; padding: 0; box-sizing: border-box; }    body { font-family: system-ui, -apple-system, sans-serif; padding: 16px; }    #output { white-space: pre-wrap; font-family: monospace; }  </style></head><body>  <div id="output"></div>  <script>    const output = document.getElementById('output');    const send = (type, message) => {      try { window.parent && window.parent.postMessage({ type, message }, '*'); } catch {}    };    const log = (...args) => {      const line = args.map(a =>        typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)      ).join(' ') + '\\n';      output.textContent += line;      send('output', line);    };    console.log = log;    console.info = log;    console.warn = log;    console.error = (...args) => {      log(...args);      send('error', args.map(String).join(' '));    };     window.addEventListener('error', (event) => {      const msg = event?.error?.stack || event?.message || 'Unknown error';      send('error', msg);    });     try {      ${code}    } catch (e) {      output.textContent = 'Error: ' + e.message;      send('error', e?.stack || e?.message || String(e));    }  </script></body></html>`; export function CodeSandbox({  code,  language,  title,  autoRun = false,  onOutput,  onError,}: CodeSandboxProps) {  const [isRunning, setIsRunning] = useState(false);  const [isFullscreen, setIsFullscreen] = useState(false);  const [copied, setCopied] = useState(false);  const [error, setError] = useState<string | null>(null);  const iframeRef = useRef<HTMLIFrameElement>(null);   const getSrcDoc = useCallback(() => {    switch (language) {      case 'html':        return code;      case 'react':        return REACT_TEMPLATE(code);      case 'javascript':        return JS_TEMPLATE(code);      default:        return code;    }  }, [code, language]);   const runCode = useCallback(() => {    setIsRunning(true);    setError(null);     if (iframeRef.current) {      try {        iframeRef.current.srcdoc = getSrcDoc();      } catch (e) {        const errorMsg = e instanceof Error ? e.message : 'Failed to run code';        setError(errorMsg);        onError?.(errorMsg);      }    }  }, [getSrcDoc, onError]);   const stopCode = useCallback(() => {    setIsRunning(false);    if (iframeRef.current) {      iframeRef.current.srcdoc = '';    }  }, []);   const copyCode = useCallback(() => {    navigator.clipboard.writeText(code);    setCopied(true);    setTimeout(() => setCopied(false), 2000);  }, [code]);   const downloadCode = useCallback(() => {    const blob = new Blob([getSrcDoc()], { type: 'text/html' });    const url = URL.createObjectURL(blob);    const a = document.createElement('a');    a.href = url;    a.download = `${title || 'artifact'}.html`;    document.body.appendChild(a);    a.click();    document.body.removeChild(a);    URL.revokeObjectURL(url);  }, [getSrcDoc, title]);   // Auto-run on mount if enabled  useEffect(() => {    if (!autoRun) return;    const id = window.setTimeout(() => {      runCode();    }, 0);    return () => window.clearTimeout(id);  }, [autoRun, runCode]);   // Listen for iframe errors  useEffect(() => {    const handleMessage = (event: MessageEvent) => {      if (event.data?.type === 'error') {        setError(event.data.message);        onError?.(event.data.message);      } else if (event.data?.type === 'output') {        onOutput?.(event.data.message);      }    };     window.addEventListener('message', handleMessage);    return () => window.removeEventListener('message', handleMessage);  }, [onError, onOutput]);   return (    <div      className={`rounded-lg border border-[var(--border)] overflow-hidden ${        isFullscreen          ? 'fixed inset-4 z-50 bg-[var(--background)]'          : ''      }`}    >      {/* Header */}      <div className="flex items-center justify-between px-3 py-2 bg-[var(--accent)] border-b border-[var(--border)]">        <div className="flex items-center gap-2">          <span className="text-xs font-medium">            {title || `${language.toUpperCase()} Preview`}          </span>          <span className="text-xs text-[var(--muted)] px-1.5 py-0.5 bg-[var(--background)] rounded">            {language}          </span>        </div>         <div className="flex items-center gap-1">          {isRunning ? (            <button              onClick={stopCode}              className="p-1.5 rounded hover:bg-[var(--error)]/20 text-[var(--error)] transition-colors"              title="Stop"            >              <Square className="h-3.5 w-3.5" />            </button>          ) : (            <button              onClick={runCode}              className="p-1.5 rounded hover:bg-[var(--success)]/20 text-[var(--success)] transition-colors"              title="Run"            >              <Play className="h-3.5 w-3.5" />            </button>          )}           <button            onClick={runCode}            className="p-1.5 rounded hover:bg-[var(--accent)] transition-colors"            title="Refresh"          >            <RefreshCw className="h-3.5 w-3.5 text-[var(--muted)]" />          </button>           <div className="w-px h-4 bg-[var(--border)] mx-1" />           <button            onClick={copyCode}            className="p-1.5 rounded hover:bg-[var(--accent)] transition-colors"            title="Copy code"          >            {copied ? (              <Check className="h-3.5 w-3.5 text-[var(--success)]" />            ) : (              <Copy className="h-3.5 w-3.5 text-[var(--muted)]" />            )}          </button>           <button            onClick={downloadCode}            className="p-1.5 rounded hover:bg-[var(--accent)] transition-colors"            title="Download"          >            <Download className="h-3.5 w-3.5 text-[var(--muted)]" />          </button>           <button            onClick={() => setIsFullscreen(!isFullscreen)}            className="p-1.5 rounded hover:bg-[var(--accent)] transition-colors"            title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}          >            {isFullscreen ? (              <Minimize2 className="h-3.5 w-3.5 text-[var(--muted)]" />            ) : (              <Maximize2 className="h-3.5 w-3.5 text-[var(--muted)]" />            )}          </button>        </div>      </div>       {/* Error Display */}      {error && (        <div className="flex items-center gap-2 px-3 py-2 bg-[var(--error)]/10 text-[var(--error)] text-xs">          <AlertTriangle className="h-3.5 w-3.5" />          {error}        </div>      )}       {/* Preview */}      <div className={`bg-white ${isFullscreen ? 'h-[calc(100%-48px)]' : 'h-64'}`}>        <iframe          ref={iframeRef}          className="w-full h-full border-0"          sandbox="allow-scripts allow-modals"          title={title || 'Code Preview'}        />      </div>    </div>  );}