Andrew-dev1.1 / components /editor /PreviewFrame.tsx
truegleai
feat: show mock UI default when project is empty
0b95feb
raw
history blame
15.1 kB
'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 &amp; 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>
)
}