Refactor remaining large components into focused modules

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-17 23:05:16 +00:00
parent 311016a0fa
commit d146e2b2a7
11 changed files with 607 additions and 470 deletions

View File

@@ -10,6 +10,9 @@ import {
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Sparkle } from '@phosphor-icons/react'
import { motion, AnimatePresence } from 'framer-motion'
import { analyzeErrorWithAI } from './analyzeError'
import { MarkdownRenderer } from './MarkdownRenderer'
import { LoadingAnalysis } from './LoadingAnalysis'
interface AIErrorHelperProps {
error: Error | string
@@ -33,20 +36,7 @@ export function AIErrorHelper({ error, context, className }: AIErrorHelperProps)
setAnalysis('')
try {
const contextInfo = context ? `\n\nContext: ${context}` : ''
const stackInfo = errorStack ? `\n\nStack trace: ${errorStack}` : ''
const prompt = (window.spark.llmPrompt as any)`You are a helpful debugging assistant for a code snippet manager app. Analyze this error and provide:
1. A clear explanation of what went wrong (in plain language)
2. Why this error likely occurred
3. 2-3 specific actionable steps to fix it
Error message: ${errorMessage}${contextInfo}${stackInfo}
Keep your response concise, friendly, and focused on practical solutions. Format your response with clear sections using markdown.`
const result = await window.spark.llm(prompt, 'gpt-4o-mini')
const result = await analyzeErrorWithAI(errorMessage, errorStack, context)
setAnalysis(result)
} catch (err) {
setAnalysisError('Unable to analyze error. The AI service may be temporarily unavailable.')
@@ -98,30 +88,7 @@ Keep your response concise, friendly, and focused on practical solutions. Format
</AlertDescription>
</Alert>
{isAnalyzing && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-muted-foreground">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
>
<Sparkle className="h-4 w-4" weight="fill" />
</motion.div>
<span className="text-sm">Analyzing error...</span>
</div>
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<motion.div
key={i}
initial={{ opacity: 0.3 }}
animate={{ opacity: [0.3, 0.6, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }}
className="h-4 bg-muted rounded"
/>
))}
</div>
</div>
)}
{isAnalyzing && <LoadingAnalysis />}
{analysisError && (
<Alert variant="destructive">
@@ -130,55 +97,7 @@ Keep your response concise, friendly, and focused on practical solutions. Format
)}
<AnimatePresence>
{analysis && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="prose prose-invert prose-sm max-w-none"
>
<div className="bg-card/50 rounded-lg p-4 border border-border space-y-3">
{analysis.split('\n').map((line, idx) => {
if (line.startsWith('###')) {
return (
<h3 key={idx} className="text-base font-semibold text-foreground mt-4 mb-2">
{line.replace('###', '').trim()}
</h3>
)
}
if (line.startsWith('##')) {
return (
<h2 key={idx} className="text-lg font-semibold text-foreground mt-4 mb-2">
{line.replace('##', '').trim()}
</h2>
)
}
if (line.match(/^\d+\./)) {
return (
<div key={idx} className="text-foreground/90 ml-2">
{line}
</div>
)
}
if (line.startsWith('-')) {
return (
<div key={idx} className="text-foreground/90 ml-4">
{line}
</div>
)
}
if (line.trim()) {
return (
<p key={idx} className="text-foreground/80 text-sm leading-relaxed">
{line}
</p>
)
}
return null
})}
</div>
</motion.div>
)}
{analysis && <MarkdownRenderer content={analysis} />}
</AnimatePresence>
</div>
</DialogContent>

View File

@@ -0,0 +1,29 @@
import { motion } from 'framer-motion'
import { Sparkle } from '@phosphor-icons/react'
export function LoadingAnalysis() {
return (
<div className="space-y-3">
<div className="flex items-center gap-2 text-muted-foreground">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
>
<Sparkle className="h-4 w-4" weight="fill" />
</motion.div>
<span className="text-sm">Analyzing error...</span>
</div>
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<motion.div
key={i}
initial={{ opacity: 0.3 }}
animate={{ opacity: [0.3, 0.6, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }}
className="h-4 bg-muted rounded"
/>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,57 @@
import { motion } from 'framer-motion'
interface MarkdownRendererProps {
content: string
}
export function MarkdownRenderer({ content }: MarkdownRendererProps) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="prose prose-invert prose-sm max-w-none"
>
<div className="bg-card/50 rounded-lg p-4 border border-border space-y-3">
{content.split('\n').map((line, idx) => {
if (line.startsWith('###')) {
return (
<h3 key={idx} className="text-base font-semibold text-foreground mt-4 mb-2">
{line.replace('###', '').trim()}
</h3>
)
}
if (line.startsWith('##')) {
return (
<h2 key={idx} className="text-lg font-semibold text-foreground mt-4 mb-2">
{line.replace('##', '').trim()}
</h2>
)
}
if (line.match(/^\d+\./)) {
return (
<div key={idx} className="text-foreground/90 ml-2">
{line}
</div>
)
}
if (line.startsWith('-')) {
return (
<div key={idx} className="text-foreground/90 ml-4">
{line}
</div>
)
}
if (line.trim()) {
return (
<p key={idx} className="text-foreground/80 text-sm leading-relaxed">
{line}
</p>
)
}
return null
})}
</div>
</motion.div>
)
}

View File

@@ -0,0 +1,21 @@
export async function analyzeErrorWithAI(
errorMessage: string,
errorStack?: string,
context?: string
): Promise<string> {
const contextInfo = context ? `\n\nContext: ${context}` : ''
const stackInfo = errorStack ? `\n\nStack trace: ${errorStack}` : ''
const prompt = (window.spark.llmPrompt as any)`You are a helpful debugging assistant for a code snippet manager app. Analyze this error and provide:
1. A clear explanation of what went wrong (in plain language)
2. Why this error likely occurred
3. 2-3 specific actionable steps to fix it
Error message: ${errorMessage}${contextInfo}${stackInfo}
Keep your response concise, friendly, and focused on practical solutions. Format your response with clear sections using markdown.`
const result = await window.spark.llm(prompt, 'gpt-4o-mini')
return result
}

View File

@@ -0,0 +1,138 @@
import { useState, useCallback } from 'react'
import { toast } from 'sonner'
import {
getDatabaseStats,
exportDatabase,
importDatabase,
clearDatabase,
seedDatabase,
validateDatabaseSchema
} from '@/lib/db'
export function useDatabaseOperations() {
const [stats, setStats] = useState<{
snippetCount: number
templateCount: number
storageType: 'indexeddb' | 'localstorage' | 'none'
databaseSize: number
} | null>(null)
const [loading, setLoading] = useState(true)
const [schemaHealth, setSchemaHealth] = useState<'unknown' | 'healthy' | 'corrupted'>('unknown')
const [checkingSchema, setCheckingSchema] = useState(false)
const loadStats = useCallback(async () => {
setLoading(true)
try {
const data = await getDatabaseStats()
setStats(data)
} catch (error) {
console.error('Failed to load stats:', error)
toast.error('Failed to load database statistics')
} finally {
setLoading(false)
}
}, [])
const checkSchemaHealth = useCallback(async () => {
setCheckingSchema(true)
try {
const result = await validateDatabaseSchema()
setSchemaHealth(result.valid ? 'healthy' : 'corrupted')
if (!result.valid) {
console.warn('Schema validation failed:', result.issues)
}
} catch (error) {
console.error('Schema check failed:', error)
setSchemaHealth('corrupted')
} finally {
setCheckingSchema(false)
}
}, [])
const handleExport = useCallback(async () => {
try {
const data = await exportDatabase()
const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `codesnippet-backup-${Date.now()}.db`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast.success('Database exported successfully')
} catch (error) {
console.error('Failed to export:', error)
toast.error('Failed to export database')
}
}, [])
const handleImport = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
try {
const arrayBuffer = await file.arrayBuffer()
const data = new Uint8Array(arrayBuffer)
await importDatabase(data)
toast.success('Database imported successfully')
await loadStats()
} catch (error) {
console.error('Failed to import:', error)
toast.error('Failed to import database')
}
event.target.value = ''
}, [loadStats])
const handleClear = useCallback(async () => {
if (!confirm('Are you sure you want to clear all data? This cannot be undone.')) {
return
}
try {
await clearDatabase()
toast.success('Database cleared and schema recreated successfully')
await loadStats()
await checkSchemaHealth()
} catch (error) {
console.error('Failed to clear:', error)
toast.error('Failed to clear database')
}
}, [loadStats, checkSchemaHealth])
const handleSeed = useCallback(async () => {
try {
await seedDatabase()
toast.success('Sample data added successfully')
await loadStats()
} catch (error) {
console.error('Failed to seed:', error)
toast.error('Failed to add sample data')
}
}, [loadStats])
const formatBytes = useCallback((bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}, [])
return {
stats,
loading,
schemaHealth,
checkingSchema,
loadStats,
checkSchemaHealth,
handleExport,
handleImport,
handleClear,
handleSeed,
formatBytes,
}
}

View File

@@ -1,260 +1,54 @@
import { useState, useEffect } from 'react'
import { toast } from 'sonner'
import {
getDatabaseStats,
exportDatabase,
importDatabase,
clearDatabase,
seedDatabase,
getAllSnippets,
validateDatabaseSchema
} from '@/lib/db'
import {
saveStorageConfig,
loadStorageConfig,
FlaskStorageAdapter,
type StorageBackend
} from '@/lib/storage'
import { useEffect } from 'react'
import { useDatabaseOperations } from './useDatabaseOperations'
import { useStorageConfig } from './useStorageConfig'
import { useStorageMigration } from './useStorageMigration'
export function useSettingsState() {
const [stats, setStats] = useState<{
snippetCount: number
templateCount: number
storageType: 'indexeddb' | 'localstorage' | 'none'
databaseSize: number
} | null>(null)
const [loading, setLoading] = useState(true)
const [storageBackend, setStorageBackend] = useState<StorageBackend>('indexeddb')
const [flaskUrl, setFlaskUrl] = useState('')
const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown')
const [testingConnection, setTestingConnection] = useState(false)
const [envVarSet, setEnvVarSet] = useState(false)
const [schemaHealth, setSchemaHealth] = useState<'unknown' | 'healthy' | 'corrupted'>('unknown')
const [checkingSchema, setCheckingSchema] = useState(false)
const {
stats,
loading,
schemaHealth,
checkingSchema,
loadStats,
checkSchemaHealth,
handleExport,
handleImport,
handleClear,
handleSeed,
formatBytes,
} = useDatabaseOperations()
const loadStats = async () => {
setLoading(true)
try {
const data = await getDatabaseStats()
setStats(data)
} catch (error) {
console.error('Failed to load stats:', error)
toast.error('Failed to load database statistics')
} finally {
setLoading(false)
}
}
const {
storageBackend,
setStorageBackend,
flaskUrl,
setFlaskUrl,
flaskConnectionStatus,
setFlaskConnectionStatus,
testingConnection,
envVarSet,
loadConfig,
handleTestConnection,
handleSaveStorageConfig: saveConfig,
} = useStorageConfig()
const testFlaskConnection = async (url: string) => {
setTestingConnection(true)
try {
const adapter = new FlaskStorageAdapter(url)
const connected = await adapter.testConnection()
setFlaskConnectionStatus(connected ? 'connected' : 'failed')
return connected
} catch (error) {
console.error('Connection test failed:', error)
setFlaskConnectionStatus('failed')
return false
} finally {
setTestingConnection(false)
}
}
const checkSchemaHealth = async () => {
setCheckingSchema(true)
try {
const result = await validateDatabaseSchema()
setSchemaHealth(result.valid ? 'healthy' : 'corrupted')
if (!result.valid) {
console.warn('Schema validation failed:', result.issues)
}
} catch (error) {
console.error('Schema check failed:', error)
setSchemaHealth('corrupted')
} finally {
setCheckingSchema(false)
}
}
const {
handleMigrateToFlask: migrateToFlask,
handleMigrateToIndexedDB,
} = useStorageMigration()
useEffect(() => {
loadStats()
checkSchemaHealth()
const config = loadStorageConfig()
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
const isEnvSet = Boolean(envFlaskUrl)
setEnvVarSet(isEnvSet)
setStorageBackend(config.backend)
setFlaskUrl(config.flaskUrl || envFlaskUrl || 'http://localhost:5000')
}, [])
const handleExport = async () => {
try {
const data = await exportDatabase()
const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `codesnippet-backup-${Date.now()}.db`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast.success('Database exported successfully')
} catch (error) {
console.error('Failed to export:', error)
toast.error('Failed to export database')
}
}
const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
try {
const arrayBuffer = await file.arrayBuffer()
const data = new Uint8Array(arrayBuffer)
await importDatabase(data)
toast.success('Database imported successfully')
await loadStats()
} catch (error) {
console.error('Failed to import:', error)
toast.error('Failed to import database')
}
event.target.value = ''
}
const handleClear = async () => {
if (!confirm('Are you sure you want to clear all data? This cannot be undone.')) {
return
}
try {
await clearDatabase()
toast.success('Database cleared and schema recreated successfully')
await loadStats()
await checkSchemaHealth()
} catch (error) {
console.error('Failed to clear:', error)
toast.error('Failed to clear database')
}
}
const handleSeed = async () => {
try {
await seedDatabase()
toast.success('Sample data added successfully')
await loadStats()
} catch (error) {
console.error('Failed to seed:', error)
toast.error('Failed to add sample data')
}
}
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}
const handleTestConnection = async () => {
await testFlaskConnection(flaskUrl)
}
loadConfig()
}, [loadStats, checkSchemaHealth, loadConfig])
const handleSaveStorageConfig = async () => {
if (storageBackend === 'flask') {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
const connected = await testFlaskConnection(flaskUrl)
if (!connected) {
toast.error('Cannot connect to Flask backend. Please check the URL and ensure the server is running.')
return
}
}
saveStorageConfig({
backend: storageBackend,
flaskUrl: storageBackend === 'flask' ? flaskUrl : undefined
})
toast.success('Storage backend updated successfully')
await loadStats()
await saveConfig(loadStats)
}
const handleMigrateToFlask = async () => {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
try {
const adapter = new FlaskStorageAdapter(flaskUrl)
const connected = await adapter.testConnection()
if (!connected) {
toast.error('Cannot connect to Flask backend')
return
}
const snippets = await getAllSnippets()
if (snippets.length === 0) {
toast.info('No snippets to migrate')
return
}
await adapter.migrateFromIndexedDB(snippets)
saveStorageConfig({
backend: 'flask',
flaskUrl
})
toast.success(`Successfully migrated ${snippets.length} snippets to Flask backend`)
await loadStats()
} catch (error) {
console.error('Migration failed:', error)
toast.error('Failed to migrate data to Flask backend')
}
}
const handleMigrateToIndexedDB = async () => {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
try {
const adapter = new FlaskStorageAdapter(flaskUrl)
const snippets = await adapter.migrateToIndexedDB()
if (snippets.length === 0) {
toast.info('No snippets to migrate')
return
}
saveStorageConfig({
backend: 'indexeddb'
})
// Full page reload is necessary here to reinitialize the database layer
// with the new backend after migration from Flask to IndexedDB
window.location.reload()
toast.success(`Successfully migrated ${snippets.length} snippets to IndexedDB`)
} catch (error) {
console.error('Migration failed:', error)
toast.error('Failed to migrate data from Flask backend')
}
await migrateToFlask(flaskUrl, loadStats)
}
return {
@@ -282,3 +76,4 @@ export function useSettingsState() {
checkSchemaHealth,
}
}

View File

@@ -0,0 +1,86 @@
import { useState, useCallback } from 'react'
import { toast } from 'sonner'
import {
saveStorageConfig,
loadStorageConfig,
FlaskStorageAdapter,
type StorageBackend
} from '@/lib/storage'
export function useStorageConfig() {
const [storageBackend, setStorageBackend] = useState<StorageBackend>('indexeddb')
const [flaskUrl, setFlaskUrl] = useState('')
const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown')
const [testingConnection, setTestingConnection] = useState(false)
const [envVarSet, setEnvVarSet] = useState(false)
const testFlaskConnection = useCallback(async (url: string) => {
setTestingConnection(true)
try {
const adapter = new FlaskStorageAdapter(url)
const connected = await adapter.testConnection()
setFlaskConnectionStatus(connected ? 'connected' : 'failed')
return connected
} catch (error) {
console.error('Connection test failed:', error)
setFlaskConnectionStatus('failed')
return false
} finally {
setTestingConnection(false)
}
}, [])
const loadConfig = useCallback(() => {
const config = loadStorageConfig()
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
const isEnvSet = Boolean(envFlaskUrl)
setEnvVarSet(isEnvSet)
setStorageBackend(config.backend)
setFlaskUrl(config.flaskUrl || envFlaskUrl || 'http://localhost:5000')
}, [])
const handleTestConnection = useCallback(async () => {
await testFlaskConnection(flaskUrl)
}, [flaskUrl, testFlaskConnection])
const handleSaveStorageConfig = useCallback(async (onSuccess?: () => Promise<void>) => {
if (storageBackend === 'flask') {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
const connected = await testFlaskConnection(flaskUrl)
if (!connected) {
toast.error('Cannot connect to Flask backend. Please check the URL and ensure the server is running.')
return
}
}
saveStorageConfig({
backend: storageBackend,
flaskUrl: storageBackend === 'flask' ? flaskUrl : undefined
})
toast.success('Storage backend updated successfully')
if (onSuccess) {
await onSuccess()
}
}, [storageBackend, flaskUrl, testFlaskConnection])
return {
storageBackend,
setStorageBackend,
flaskUrl,
setFlaskUrl,
flaskConnectionStatus,
setFlaskConnectionStatus,
testingConnection,
envVarSet,
loadConfig,
handleTestConnection,
handleSaveStorageConfig,
}
}

View File

@@ -0,0 +1,84 @@
import { useCallback } from 'react'
import { toast } from 'sonner'
import { getAllSnippets } from '@/lib/db'
import {
saveStorageConfig,
FlaskStorageAdapter
} from '@/lib/storage'
export function useStorageMigration() {
const handleMigrateToFlask = useCallback(async (flaskUrl: string, onSuccess?: () => Promise<void>) => {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
try {
const adapter = new FlaskStorageAdapter(flaskUrl)
const connected = await adapter.testConnection()
if (!connected) {
toast.error('Cannot connect to Flask backend')
return
}
const snippets = await getAllSnippets()
if (snippets.length === 0) {
toast.info('No snippets to migrate')
return
}
await adapter.migrateFromIndexedDB(snippets)
saveStorageConfig({
backend: 'flask',
flaskUrl
})
toast.success(`Successfully migrated ${snippets.length} snippets to Flask backend`)
if (onSuccess) {
await onSuccess()
}
} catch (error) {
console.error('Migration failed:', error)
toast.error('Failed to migrate data to Flask backend')
}
}, [])
const handleMigrateToIndexedDB = useCallback(async (flaskUrl: string) => {
if (!flaskUrl) {
toast.error('Please enter a Flask backend URL')
return
}
try {
const adapter = new FlaskStorageAdapter(flaskUrl)
const snippets = await adapter.migrateToIndexedDB()
if (snippets.length === 0) {
toast.info('No snippets to migrate')
return
}
saveStorageConfig({
backend: 'indexeddb'
})
// Full page reload is necessary here to reinitialize the database layer
// with the new backend after migration from Flask to IndexedDB
window.location.reload()
toast.success(`Successfully migrated ${snippets.length} snippets to IndexedDB`)
} catch (error) {
console.error('Migration failed:', error)
toast.error('Failed to migrate data from Flask backend')
}
}, [])
return {
handleMigrateToFlask,
handleMigrateToIndexedDB,
}
}

View File

@@ -0,0 +1,34 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export function DemoFeatureCards() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-primary/20">
<CardHeader>
<CardTitle className="text-lg">Real-Time Updates</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Watch your React components render instantly as you type. No refresh needed.
</CardContent>
</Card>
<Card className="border-accent/20">
<CardHeader>
<CardTitle className="text-lg">Resizable Panels</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Drag the center divider to adjust the editor and preview panel sizes to your preference.
</CardContent>
</Card>
<Card className="border-primary/20">
<CardHeader>
<CardTitle className="text-lg">Multiple View Modes</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Switch between code-only, split-screen, or preview-only modes with the toggle buttons.
</CardContent>
</Card>
</div>
)
}

View File

@@ -3,117 +3,8 @@ import { motion } from 'framer-motion'
import { SplitScreenEditor } from '@/components/features/snippet-editor/SplitScreenEditor'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Sparkle } from '@phosphor-icons/react'
const DEMO_CODE = `function Counter() {
const [count, setCount] = React.useState(0)
return (
<div style={{
padding: '2rem',
fontFamily: 'Inter, sans-serif',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '1.5rem'
}}>
<h2 style={{
fontSize: '2rem',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #8b5cf6, #06b6d4)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
margin: 0
}}>
Interactive Counter
</h2>
<div style={{
fontSize: '4rem',
fontWeight: 'bold',
color: '#8b5cf6',
padding: '2rem',
background: 'rgba(139, 92, 246, 0.1)',
borderRadius: '1rem',
minWidth: '200px',
textAlign: 'center'
}}>
{count}
</div>
<div style={{ display: 'flex', gap: '1rem' }}>
<button
onClick={() => setCount(count - 1)}
style={{
padding: '0.75rem 2rem',
fontSize: '1.125rem',
fontWeight: '600',
background: '#ef4444',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
Decrement
</button>
<button
onClick={() => setCount(0)}
style={{
padding: '0.75rem 2rem',
fontSize: '1.125rem',
fontWeight: '600',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
Reset
</button>
<button
onClick={() => setCount(count + 1)}
style={{
padding: '0.75rem 2rem',
fontSize: '1.125rem',
fontWeight: '600',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
Increment
</button>
</div>
<p style={{
marginTop: '1rem',
color: '#9ca3af',
fontSize: '0.875rem'
}}>
Try editing the code on the left to see live changes!
</p>
</div>
)
}
Counter`
import { DEMO_CODE } from './demo-constants'
import { DemoFeatureCards } from './DemoFeatureCards'
export function DemoPage() {
const [code, setCode] = useState(DEMO_CODE)
@@ -158,34 +49,7 @@ export function DemoPage() {
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-primary/20">
<CardHeader>
<CardTitle className="text-lg">Real-Time Updates</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Watch your React components render instantly as you type. No refresh needed.
</CardContent>
</Card>
<Card className="border-accent/20">
<CardHeader>
<CardTitle className="text-lg">Resizable Panels</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Drag the center divider to adjust the editor and preview panel sizes to your preference.
</CardContent>
</Card>
<Card className="border-primary/20">
<CardHeader>
<CardTitle className="text-lg">Multiple View Modes</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Switch between code-only, split-screen, or preview-only modes with the toggle buttons.
</CardContent>
</Card>
</div>
<DemoFeatureCards />
</motion.div>
)
}

110
src/pages/demo-constants.ts Normal file
View File

@@ -0,0 +1,110 @@
export const DEMO_CODE = `function Counter() {
const [count, setCount] = React.useState(0)
return (
<div style={{
padding: '2rem',
fontFamily: 'Inter, sans-serif',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '1.5rem'
}}>
<h2 style={{
fontSize: '2rem',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #8b5cf6, #06b6d4)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
margin: 0
}}>
Interactive Counter
</h2>
<div style={{
fontSize: '4rem',
fontWeight: 'bold',
color: '#8b5cf6',
padding: '2rem',
background: 'rgba(139, 92, 246, 0.1)',
borderRadius: '1rem',
minWidth: '200px',
textAlign: 'center'
}}>
{count}
</div>
<div style={{ display: 'flex', gap: '1rem' }}>
<button
onClick={() => setCount(count - 1)}
style={{
padding: '0.75rem 2rem',
fontSize: '1.125rem',
fontWeight: '600',
background: '#ef4444',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
Decrement
</button>
<button
onClick={() => setCount(0)}
style={{
padding: '0.75rem 2rem',
fontSize: '1.125rem',
fontWeight: '600',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
Reset
</button>
<button
onClick={() => setCount(count + 1)}
style={{
padding: '0.75rem 2rem',
fontSize: '1.125rem',
fontWeight: '600',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
>
Increment
</button>
</div>
<p style={{
marginTop: '1rem',
color: '#9ca3af',
fontSize: '0.875rem'
}}>
Try editing the code on the left to see live changes!
</p>
</div>
)
}
Counter`