Spaces:
Paused
Paused
| 'use client' | |
| import { useState, useEffect, useRef } from 'react' | |
| import { RefreshCw, ExternalLink } from 'lucide-react' | |
| interface PreviewFrameProps { | |
| code: string | |
| } | |
| // Shown when the project has no HTML/CSS/JS content yet | |
| const MOCK_UI_HTML = `<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| background: #0a0a0f; | |
| color: #fff; | |
| font-family: system-ui, -apple-system, sans-serif; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Animated background grid */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background-image: | |
| linear-gradient(rgba(102,126,234,0.06) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(102,126,234,0.06) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| z-index: 0; | |
| } | |
| .container { | |
| position: relative; | |
| z-index: 1; | |
| max-width: 480px; | |
| margin: 0 auto; | |
| padding: 24px 16px; | |
| } | |
| /* Nav bar mock */ | |
| .nav { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 14px 20px; | |
| background: rgba(255,255,255,0.03); | |
| border: 1px solid rgba(255,255,255,0.08); | |
| border-radius: 14px; | |
| margin-bottom: 20px; | |
| backdrop-filter: blur(12px); | |
| } | |
| .nav-logo { | |
| width: 28px; height: 28px; | |
| border-radius: 8px; | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| } | |
| .nav-links { display: flex; gap: 12px; } | |
| .nav-link { | |
| width: 48px; height: 8px; | |
| border-radius: 4px; | |
| background: rgba(255,255,255,0.12); | |
| } | |
| .nav-btn { | |
| width: 72px; height: 28px; | |
| border-radius: 8px; | |
| background: linear-gradient(135deg, #667eea44, #764ba244); | |
| border: 1px solid rgba(102,126,234,0.3); | |
| } | |
| /* Hero section */ | |
| .hero { | |
| text-align: center; | |
| padding: 32px 0 24px; | |
| } | |
| .hero-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 12px; | |
| border-radius: 999px; | |
| background: rgba(102,126,234,0.15); | |
| border: 1px solid rgba(102,126,234,0.3); | |
| font-size: 11px; | |
| color: #8b9cff; | |
| margin-bottom: 16px; | |
| animation: fadeUp 0.6s ease forwards; | |
| } | |
| .hero-badge::before { content: '✦'; font-size: 8px; } | |
| .hero-title { | |
| font-size: 28px; | |
| font-weight: 800; | |
| line-height: 1.2; | |
| margin-bottom: 12px; | |
| background: linear-gradient(135deg, #fff 40%, #8b9cff); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| animation: fadeUp 0.6s ease 0.1s both; | |
| } | |
| .hero-sub { | |
| font-size: 13px; | |
| color: rgba(255,255,255,0.45); | |
| line-height: 1.6; | |
| max-width: 300px; | |
| margin: 0 auto 24px; | |
| animation: fadeUp 0.6s ease 0.2s both; | |
| } | |
| .hero-actions { | |
| display: flex; | |
| gap: 10px; | |
| justify-content: center; | |
| animation: fadeUp 0.6s ease 0.3s both; | |
| } | |
| .btn-primary { | |
| padding: 10px 22px; | |
| border-radius: 10px; | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #fff; | |
| border: none; | |
| cursor: pointer; | |
| box-shadow: 0 8px 24px rgba(102,126,234,0.35); | |
| } | |
| .btn-secondary { | |
| padding: 10px 22px; | |
| border-radius: 10px; | |
| background: rgba(255,255,255,0.05); | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: rgba(255,255,255,0.7); | |
| border: 1px solid rgba(255,255,255,0.12); | |
| cursor: pointer; | |
| } | |
| /* Stats row */ | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| animation: fadeUp 0.6s ease 0.4s both; | |
| } | |
| .stat-card { | |
| padding: 14px 12px; | |
| border-radius: 12px; | |
| background: rgba(255,255,255,0.03); | |
| border: 1px solid rgba(255,255,255,0.07); | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 18px; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, #8b9cff, #c084fc); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .stat-label { | |
| font-size: 10px; | |
| color: rgba(255,255,255,0.35); | |
| margin-top: 2px; | |
| } | |
| /* Feature cards */ | |
| .cards { display: flex; flex-direction: column; gap: 10px; animation: fadeUp 0.6s ease 0.5s both; } | |
| .card { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| padding: 16px; | |
| border-radius: 14px; | |
| background: rgba(255,255,255,0.03); | |
| border: 1px solid rgba(255,255,255,0.07); | |
| transition: border-color 0.3s; | |
| } | |
| .card:hover { border-color: rgba(102,126,234,0.3); } | |
| .card-icon { | |
| width: 40px; height: 40px; | |
| border-radius: 10px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 18px; | |
| flex-shrink: 0; | |
| } | |
| .card-icon.purple { background: rgba(102,126,234,0.15); } | |
| .card-icon.pink { background: rgba(236,72,153,0.15); } | |
| .card-icon.cyan { background: rgba(0,212,255,0.15); } | |
| .card-title { font-size: 13px; font-weight: 600; margin-bottom: 3px; } | |
| .card-desc { font-size: 11px; color: rgba(255,255,255,0.4); line-height: 1.4; } | |
| .card-arrow { | |
| margin-left: auto; | |
| color: rgba(255,255,255,0.2); | |
| font-size: 14px; | |
| flex-shrink: 0; | |
| } | |
| /* Empty state hint */ | |
| .hint { | |
| margin-top: 24px; | |
| padding: 16px; | |
| border-radius: 12px; | |
| background: rgba(102,126,234,0.08); | |
| border: 1px dashed rgba(102,126,234,0.25); | |
| text-align: center; | |
| animation: fadeUp 0.6s ease 0.6s both; | |
| } | |
| .hint-title { font-size: 12px; font-weight: 600; color: #8b9cff; margin-bottom: 4px; } | |
| .hint-sub { font-size: 11px; color: rgba(255,255,255,0.35); line-height: 1.5; } | |
| /* Progress bar mock */ | |
| .progress-section { | |
| margin-bottom: 20px; | |
| animation: fadeUp 0.6s ease 0.45s both; | |
| } | |
| .progress-row { | |
| display: flex; align-items: center; justify-content: space-between; | |
| margin-bottom: 6px; | |
| } | |
| .progress-label { font-size: 11px; color: rgba(255,255,255,0.5); } | |
| .progress-val { font-size: 11px; color: #8b9cff; font-weight: 600; } | |
| .progress-track { | |
| height: 6px; | |
| background: rgba(255,255,255,0.07); | |
| border-radius: 999px; | |
| overflow: hidden; | |
| margin-bottom: 10px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| border-radius: 999px; | |
| animation: fillBar 1.4s cubic-bezier(.4,0,.2,1) forwards; | |
| } | |
| @keyframes fillBar { from { width: 0; } } | |
| .fill-a { background: linear-gradient(90deg,#667eea,#8b9cff); width: 78%; animation-delay: 0.5s; } | |
| .fill-b { background: linear-gradient(90deg,#c084fc,#f472b6); width: 52%; animation-delay: 0.7s; } | |
| .fill-c { background: linear-gradient(90deg,#00d4ff,#00ff88); width: 91%; animation-delay: 0.9s; } | |
| @keyframes fadeUp { | |
| from { opacity: 0; transform: translateY(16px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- Nav --> | |
| <div class="nav"> | |
| <div class="nav-logo"></div> | |
| <div class="nav-links"> | |
| <div class="nav-link"></div> | |
| <div class="nav-link"></div> | |
| <div class="nav-link"></div> | |
| </div> | |
| <div class="nav-btn"></div> | |
| </div> | |
| <!-- Hero --> | |
| <div class="hero"> | |
| <div class="hero-badge">Open-View Editor</div> | |
| <div class="hero-title">Your canvas<br>starts here</div> | |
| <div class="hero-sub">Write HTML, CSS & JS in the Code tab — or use AI Chat and Design to build something amazing.</div> | |
| <div class="hero-actions"> | |
| <button class="btn-primary">Get Started</button> | |
| <button class="btn-secondary">View Docs</button> | |
| </div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="stats"> | |
| <div class="stat-card"> | |
| <div class="stat-value">14+</div> | |
| <div class="stat-label">Presets</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">3D</div> | |
| <div class="stat-label">Three.js</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value">AI</div> | |
| <div class="stat-label">Powered</div> | |
| </div> | |
| </div> | |
| <!-- Progress bars --> | |
| <div class="progress-section"> | |
| <div class="progress-row"> | |
| <span class="progress-label">HTML</span> | |
| <span class="progress-val">78%</span> | |
| </div> | |
| <div class="progress-track"><div class="progress-fill fill-a"></div></div> | |
| <div class="progress-row"> | |
| <span class="progress-label">CSS</span> | |
| <span class="progress-val">52%</span> | |
| </div> | |
| <div class="progress-track"><div class="progress-fill fill-b"></div></div> | |
| <div class="progress-row"> | |
| <span class="progress-label">JavaScript</span> | |
| <span class="progress-val">91%</span> | |
| </div> | |
| <div class="progress-track"><div class="progress-fill fill-c"></div></div> | |
| </div> | |
| <!-- Feature cards --> | |
| <div class="cards"> | |
| <div class="card"> | |
| <div class="card-icon purple">✦</div> | |
| <div> | |
| <div class="card-title">AI Chat</div> | |
| <div class="card-desc">Ask the AI to write or modify your code</div> | |
| </div> | |
| <div class="card-arrow">›</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon pink">◈</div> | |
| <div> | |
| <div class="card-title">Design Library</div> | |
| <div class="card-desc">Pick presets, tweak sliders, apply instantly</div> | |
| </div> | |
| <div class="card-arrow">›</div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon cyan">⬡</div> | |
| <div> | |
| <div class="card-title">Three.js 3D</div> | |
| <div class="card-desc">Particles, cubes, waves — one click away</div> | |
| </div> | |
| <div class="card-arrow">›</div> | |
| </div> | |
| </div> | |
| <!-- Hint --> | |
| <div class="hint"> | |
| <div class="hint-title">👆 This is your preview</div> | |
| <div class="hint-sub">Start typing in the Code tab or apply a Design preset — your changes appear here instantly.</div> | |
| </div> | |
| </div> | |
| </body> | |
| </html>` | |
| // Detect if the project has any real user content | |
| function isEmptyProject(code: string): boolean { | |
| // Extract content between <body> tags | |
| const bodyMatch = code.match(/<body[^>]*>([\s\S]*?)<\/body>/i) | |
| if (!bodyMatch) return true | |
| const bodyContent = bodyMatch[1] | |
| .replace(/<script[\s\S]*?<\/script>/gi, '') // strip script tags | |
| .trim() | |
| return bodyContent.length === 0 | |
| } | |
| export function PreviewFrame({ code }: PreviewFrameProps) { | |
| const iframeRef = useRef<HTMLIFrameElement>(null) | |
| const [isLoading, setIsLoading] = useState(true) | |
| const [isFullscreen, setIsFullscreen] = useState(false) | |
| const displayCode = isEmptyProject(code) ? MOCK_UI_HTML : code | |
| useEffect(() => { | |
| if (iframeRef.current) { | |
| setIsLoading(true) | |
| const iframe = iframeRef.current | |
| const doc = iframe.contentDocument || iframe.contentWindow?.document | |
| if (doc) { | |
| doc.open() | |
| doc.write(displayCode) | |
| doc.close() | |
| iframe.onload = () => setIsLoading(false) | |
| } | |
| } | |
| }, [displayCode]) | |
| const refreshPreview = () => { | |
| if (iframeRef.current) { | |
| setIsLoading(true) | |
| const iframe = iframeRef.current | |
| const doc = iframe.contentDocument || iframe.contentWindow?.document | |
| if (doc) { | |
| doc.open() | |
| doc.write(displayCode) | |
| doc.close() | |
| setTimeout(() => setIsLoading(false), 300) | |
| } | |
| } | |
| } | |
| if (isFullscreen) { | |
| return ( | |
| <div className="fixed inset-0 z-[100] bg-white"> | |
| <button | |
| onClick={() => setIsFullscreen(false)} | |
| className="absolute top-4 right-4 z-10 p-2 rounded-lg bg-dark-900/80 text-white" | |
| > | |
| Exit Fullscreen | |
| </button> | |
| <iframe | |
| ref={iframeRef} | |
| className="w-full h-full border-0" | |
| sandbox="allow-scripts allow-modals allow-forms allow-same-origin" | |
| srcDoc={displayCode} | |
| /> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="h-full flex flex-col" style={{ background: 'rgba(10,10,10,0.9)' }}> | |
| {/* Preview Toolbar */} | |
| <div | |
| className="flex items-center justify-between px-4 py-2" | |
| style={{ | |
| borderBottom: '1px solid rgba(255,255,255,0.1)', | |
| background: 'rgba(255,255,255,0.02)', | |
| }} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm font-medium" style={{ color: 'rgba(255,255,255,0.7)' }}>Preview</span> | |
| {isEmptyProject(code) && ( | |
| <span | |
| className="text-xs px-2 py-0.5 rounded-full" | |
| style={{ | |
| background: 'rgba(102,126,234,0.15)', | |
| color: '#8b9cff', | |
| border: '1px solid rgba(102,126,234,0.25)', | |
| }} | |
| > | |
| Default UI | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={refreshPreview} | |
| className="p-2 rounded-lg transition-colors" | |
| style={{ | |
| background: 'rgba(255,255,255,0.03)', | |
| border: '1px solid rgba(255,255,255,0.1)', | |
| minWidth: '44px', | |
| minHeight: '44px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| }} | |
| title="Refresh" | |
| > | |
| <RefreshCw className="w-4 h-4" style={{ color: 'rgba(255,255,255,0.6)' }} /> | |
| </button> | |
| <button | |
| onClick={() => setIsFullscreen(true)} | |
| className="p-2 rounded-lg transition-colors" | |
| style={{ | |
| background: 'rgba(255,255,255,0.03)', | |
| border: '1px solid rgba(255,255,255,0.1)', | |
| minWidth: '44px', | |
| minHeight: '44px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| }} | |
| title="Fullscreen" | |
| > | |
| <ExternalLink className="w-4 h-4" style={{ color: 'rgba(255,255,255,0.6)' }} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Iframe */} | |
| <div className="flex-1 relative" style={{ background: '#0a0a0f' }}> | |
| {isLoading && ( | |
| <div className="absolute inset-0 flex items-center justify-center bg-black/80 z-10"> | |
| <div className="animate-spin rounded-full h-8 w-8 border-2 border-primary-500 border-t-transparent" /> | |
| </div> | |
| )} | |
| <iframe | |
| ref={iframeRef} | |
| className="w-full h-full border-0" | |
| sandbox="allow-scripts allow-modals allow-forms allow-same-origin" | |
| srcDoc={displayCode} | |
| /> | |
| </div> | |
| </div> | |
| ) | |
| } | |