diff --git a/app/page.tsx b/app/page.tsx
index d3c5c18..72355f1 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,4 +1,4 @@
-"use client"
+"use client"
const safeBase64Encode = (str: string) =>
btoa(unescape(encodeURIComponent(str)));
@@ -7,29 +7,24 @@ const safeBase64Decode = (str: string) =>
decodeURIComponent(escape(atob(str)));
import type React from "react"
-import { useState, useEffect, useRef } from "react"
+import { useState, useEffect, useMemo, useRef, useCallback } from "react"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-
+import { Badge } from "@/components/ui/badge"
+import { CopyButton } from "@/components/ui/copy-button"
+import { CommandPalette, type Command } from "@/components/ui/command-palette"
import {
- Code2,
- Play,
- Download,
- Layout,
- FileText,
- Palette,
- Zap,
- Sun,
- Moon,
- Link as LinkIcon,
- Timer,
+ Code2, Play, Download, Upload, Layout, Maximize2, Minimize2,
+ FileText, Palette, Zap, Sun, Moon, Search, Link as LinkIcon,
+ Undo2, Redo2, Timer, MoreHorizontal, X,
} from "lucide-react"
import { toast } from "sonner"
-
-
-
-
+import * as prettier from 'prettier'
+import parserHtml from 'prettier/plugins/html'
+import parserCss from 'prettier/plugins/postcss'
+import parserBabel from 'prettier/plugins/babel'
+import parserEstree from 'prettier/plugins/estree'
import JSZip from "jszip"
import dynamic from "next/dynamic"
import Link from "next/link"
@@ -38,6 +33,7 @@ import {
PreviewErrorBoundary,
AppErrorBoundary,
} from "./components/error-boundary"
+import AIAssistant from "./components/AIAssistant"
const MonacoEditor = dynamic(() => import("./components/monaco-editor"), {
ssr: false,
@@ -120,929 +116,9 @@ const templates: Template[] = [
description: "Modern landing page template",
icon: ,
content: {
-
- html: `
-
-
-
-
- Modern Landing Page
-
-
-
-
-
-
-
Welcome to the Future
-
Build amazing things with our platform
-
Get Started
-
-
-
-
-
-
Amazing Features
-
Everything you need to build high-performance applications with ease
-
-
-
-
Lightning Fast
-
Experience blazing-fast render times and optimized resource delivery for peak performance.
-
-
-
-
Secure by Design
-
Your data is protected with end-to-end encryption, strict compliance, and active threat monitoring.
-
-
-
-
-
-
Advanced Analytics
-
Gain deeper insights into user engagement, system health, and growth metrics in real-time.
-
-
-
-
-
-
-
-
-
About Our Platform
-
We are dedicated to building a platform that empowers developers and creators. By focusing on cutting-edge technologies, we eliminate complex configurations so you can focus purely on what matters: your code.
-
Our platform handles scaling, global CDN edge caching, and automated builds, allowing you to deploy dynamic, beautiful web applications with just one click.
-
-
-
-
Collaborative developer workflows
-
-
-
-
Automatic scaling & edge routing
-
-
-
-
Integrated analytics and logging
-
-
-
-
-
-
-
-
-
-
What Our Users Say
-
Join thousands of developers and teams already building the future on our platform
-
-
-
★★★★★
-
"Brand has completely transformed our workflow. The setup was instant, and the interface is incredibly smooth. Deploying landing pages takes seconds now!"
-
-
SC
-
-
Sarah Connor
- Lead Architect, TechCorp
-
-
-
-
-
★★★★★
-
"The performance boost we saw after migrating to this platform was unbelievable. Plus, the built-in analytics are actually useful rather than bloated."
-
-
DM
-
-
David Miller
- Product Manager, Innovate
-
-
-
-
-
★★★★★
-
"Support is responsive, the documentation is clear, and the developer experience is unmatched. I can't recommend this platform enough."
-
-
ER
-
-
Elena Rostova
- CTO, FutureFlow
-
-
-
-
-
-
-
-
-
-`,
- css: `* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- line-height: 1.6;
-}
-
-.header {
- color: white;
- background: #130a2e;
- padding: 1rem 0;
- position: fixed;
- width: 100%;
- top: 0;
- z-index: 1000;
-}
-
-.nav {
- display: flex;
- justify-content: space-between;
- align-items: center;
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 2rem;
-}
-
-.logo {
- font-size: 1.5rem;
- font-weight: bold;
-}
-
-.nav-links {
- display: flex;
- list-style: none;
- gap: 2rem;
-}
-
-.nav-links a {
- color: white;
- text-decoration: none;
- transition: opacity 0.3s;
-}
-
-.nav-links a:hover {
- opacity: 0.8;
-}
-
-.hero {
- height: 100vh;
- background-color: #130a2e;
- background-image:
- radial-gradient(circle at 15% 50%, rgba(102, 126, 234, 0.15), transparent 25%),
- radial-gradient(circle at 85% 30%, rgba(118, 75, 162, 0.15), transparent 25%);
- display: flex;
- align-items: center;
- justify-content: center;
- text-align: center;
- color: white;
- position: relative;
- overflow: hidden;
-}
-
-.hero-content {
- max-width: 600px;
- padding: 2rem;
- position: relative;
- z-index: 1;
-}
-
-.hero-title {
- font-size: 4rem;
- font-weight: 800;
- margin-bottom: 1.25rem;
- background: linear-gradient(to right, #ffffff, #c5bedb);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: fadeInUp 1s ease-out;
-}
-
-.hero-subtitle {
- font-size: 1.25rem;
- color: #c5bedb;
- margin-bottom: 2.5rem;
- animation: fadeInUp 1s ease-out 0.2s both;
-}
-
-.cta-button {
- color: #130a2e;
- font-weight: 700;
- border: none;
- padding: 1rem 2.5rem;
- font-size: 1.1rem;
- border-radius: 50px;
- cursor: pointer;
- transition: all 0.3s ease;
- animation: fadeInUp 1s ease-out 0.4s both;
-}
-
-.cta-button:hover {
- transform: translateY(-3px) scale(1.02);
- box-shadow: 0 15px 25px -5px rgba(102, 126, 234, 0.6);
-}
-
-.section {
- padding: 6rem 2rem;
- background: #130a2e;
- color: #ffffff;
- display: flex;
- justify-content: center;
- align-items: center;
- scroll-margin-top: 70px;
-}
-
-.container {
- width: 100%;
- max-width: 1200px;
- margin: 0 auto;
-}
-
-.section-title {
- font-size: 2.5rem;
- text-align: center;
- margin-bottom: 0.5rem;
- color: #ffffff;
- font-weight: 800;
-}
-
-.section-title.text-left {
- text-align: left;
-}
-
-.section-subtitle {
- font-size: 1.1rem;
- text-align: center;
- color: #c5bedb;
- margin-bottom: 3.5rem;
- max-width: 700px;
- margin-left: auto;
- margin-right: auto;
-}
-
-.features {
- background: #130a2e;
-}
-
-.features-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 2.5rem;
-}
-
-.feature-card {
- background: #21134a;
- border: 1px solid #3c257d;
- border-radius: 20px;
- padding: 2.5rem 2rem;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.feature-card:hover {
- transform: translateY(-8px);
- box-shadow: 0 15px 35px -10px rgba(102, 126, 234, 0.3);
- border-color: #667eea;
-}
-
-.feature-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 50px;
- height: 50px;
- border-radius: 12px;
- background: rgba(102, 126, 234, 0.15);
- color: #00f2fe;
- margin-bottom: 1.5rem;
- transition: all 0.3s ease;
-}
-
-.feature-card:hover .feature-icon {
- background: linear-gradient(135deg, #00f2fe 0%, #667eea 100%);
- color: #130a2e;
-}
-
-.feature-card h3 {
- font-size: 1.35rem;
- margin-bottom: 0.75rem;
- color: #ffffff;
- font-weight: 700;
-}
-
-.feature-card p {
- color: #c5bedb;
- font-size: 0.95rem;
- line-height: 1.6;
-}
-
-.about {
- background: #170d37;
-}
-
-.about-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- max-width: 800px;
- margin: 0 auto;
-}
-
-.about-content h2 {
- margin-bottom: 1.5rem;
-}
-
-.about-content p {
- color: #c5bedb;
- font-size: 1.05rem;
- line-height: 1.7;
- margin-bottom: 1.5rem;
-}
-
-.about-points {
- margin-top: 2rem;
- display: inline-flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 1rem;
-}
-
-.about-point {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.check-icon {
- color: #00f2fe;
- flex-shrink: 0;
-}
-
-.about-point span {
- font-size: 0.95rem;
- font-weight: 600;
- color: #ffffff;
-}
-
-/* Cleaned up removed SVG wrapper classes */
-
-.testimonials {
- background: #130a2e;
-}
-
-.testimonials-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
- gap: 2.5rem;
-}
-
-.testimonial-card {
- background: #21134a;
- border: 1px solid #3c257d;
- border-radius: 20px;
- padding: 2.5rem;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- transition: all 0.3s ease;
-}
-
-.testimonial-card:hover {
- box-shadow: 0 15px 35px -10px rgba(102, 126, 234, 0.3);
- border-color: #667eea;
-}
-
-.stars {
- color: #f59e0b;
- font-size: 1.1rem;
- margin-bottom: 1rem;
-}
-
-.testimonial-text {
- font-size: 1rem;
- color: #c5bedb;
- font-style: italic;
- line-height: 1.6;
- margin-bottom: 2rem;
-}
-
-.user-info {
- display: flex;
- align-items: center;
- gap: 1rem;
-}
-
-.avatar {
- width: 44px;
- height: 44px;
- border-radius: 50%;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- font-size: 0.9rem;
-}
-
-.user-info h4 {
- font-size: 0.95rem;
- color: #ffffff;
- margin-bottom: 0.15rem;
-}
-
-.user-info span {
- font-size: 0.8rem;
- color: #c5bedb;
-}
-
-.footer {
- background: #0b061d;
- color: #a59ec0;
- padding: 5rem 2rem 2rem;
- border-top: 1px solid #21134a;
-}
-
-.footer-container {
- display: grid;
- grid-template-columns: 2fr 1fr 1fr 1.2fr;
- gap: 4rem;
- margin-bottom: 4rem;
-}
-
-.footer-brand {
- display: flex;
- flex-direction: column;
- gap: 1.25rem;
-}
-
-.footer-brand .logo {
- color: white;
-}
-
-.footer-brand p {
- font-size: 0.95rem;
- line-height: 1.6;
- max-width: 320px;
-}
-
-.social-icons {
- display: flex;
- gap: 1rem;
-}
-
-.social-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 38px;
- height: 38px;
- border-radius: 50%;
- background: #21134a;
- color: #a59ec0;
- transition: all 0.3s ease;
-}
-
-.social-icon:hover {
- background: linear-gradient(135deg, #00f2fe 0%, #667eea 100%);
- color: #130a2e;
- transform: translateY(-3px);
-}
-
-.footer-links h4 {
- color: white;
- font-size: 1.05rem;
- font-weight: 600;
- margin-bottom: 1.5rem;
-}
-
-.footer-links ul {
- list-style: none;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.footer-links a {
- color: #a59ec0;
- text-decoration: none;
- font-size: 0.95rem;
- transition: color 0.3s ease;
-}
-
-.footer-links a:hover {
- color: white;
-}
-
-.footer-links li {
- font-size: 0.95rem;
- line-height: 1.5;
-}
-
-.footer-bottom {
- border-top: 1px solid #21134a;
- padding-top: 2rem;
-}
-
-.footer-bottom-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 1rem;
- font-size: 0.875rem;
-}
-
-.footer-legal {
- display: flex;
- gap: 1rem;
- align-items: center;
-}
-
-.footer-legal a {
- color: #a59ec0;
- text-decoration: none;
- transition: color 0.3s ease;
-}
-
-.footer-legal a:hover {
- color: white;
-}
-
-@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(30px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@media (max-width: 968px) {
- .footer-container {
- grid-template-columns: 1fr 1fr;
- gap: 3rem;
- }
-}
-
-@media (max-width: 768px) {
-/* About section media queries removed as it is now centered by default */
- .section {
- padding: 4rem 1.5rem;
- }
- .footer-container {
- grid-template-columns: 1fr;
- gap: 2.5rem;
- }
- .footer-bottom-container {
- flex-direction: column;
- text-align: center;
- }
-}
-`,
- javascript: `function handleCTA() {
- alert('Welcome! This is where you would redirect to signup or more info.');
-}
-
-// Add smooth scrolling for navigation links
-document.addEventListener('DOMContentLoaded', function() {
- const navLinks = document.querySelectorAll('.nav-links a');
-
- navLinks.forEach(link => {
- link.addEventListener('click', function(e) {
- const targetId = this.getAttribute('href');
- const targetElement = document.querySelector(targetId);
-
- if (targetElement) {
- targetElement.scrollIntoView({
- behavior: 'smooth'
- });
- }
- });
- });
-});
-
-/* Interactive effects script removed */`,
- },
- },
- {
- id: "interactive-card",
- name: "Interactive Card",
- description: "Animated card component",
- icon: ,
- content: {
- html: `
-
-
-
-
- Interactive Card
-
-
-
-
-
-
-
Hover over me to see the magic happen!
-
-
- 42
- Projects
-
-
- 1.2k
- Users
-
-
-
-
-
-
-
-`,
- css: `body {
- margin: 0;
- padding: 0;
- min-height: 100vh;
- background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.container {
- perspective: 1000px;
-}
-
-.card {
- width: 350px;
- background: rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(10px);
- border-radius: 20px;
- padding: 2rem;
- color: white;
- border: 1px solid rgba(255, 255, 255, 0.2);
- transition: all 0.3s ease;
- cursor: pointer;
- position: relative;
- overflow: hidden;
-}
-
-.card::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
- transition: left 0.5s;
-}
-
-.card:hover::before {
- left: 100%;
-}
-
-.card:hover {
- transform: translateY(-10px) rotateX(5deg);
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
-}
-
-.card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
-}
-
-.card-header h2 {
- margin: 0;
- font-size: 1.5rem;
-}
-
-.status {
- background: #4ade80;
- padding: 0.25rem 0.75rem;
- border-radius: 20px;
- font-size: 0.8rem;
- font-weight: 600;
-}
-
-.card-content p {
- margin-bottom: 1.5rem;
- opacity: 0.9;
- line-height: 1.6;
-}
-
-.stats {
- display: flex;
- gap: 2rem;
- margin-bottom: 1.5rem;
-}
-
-.stat {
- text-align: center;
-}
-
-.stat-number {
- display: block;
- font-size: 2rem;
- font-weight: bold;
- color: #4ade80;
-}
-
-.stat-label {
- font-size: 0.9rem;
- opacity: 0.8;
-}
-
-.card-footer {
- display: flex;
- gap: 1rem;
-}
-
-.btn-primary, .btn-secondary {
- padding: 0.75rem 1.5rem;
- border: none;
- border-radius: 10px;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.3s ease;
- flex: 1;
-}
-
-.btn-primary {
- background: #4ade80;
- color: #1f2937;
-}
-
-.btn-primary:hover {
- background: #22c55e;
- transform: translateY(-2px);
-}
-
-.btn-secondary {
- background: transparent;
- color: white;
- border: 1px solid rgba(255, 255, 255, 0.3);
-}
-
-.btn-secondary:hover {
- background: rgba(255, 255, 255, 0.1);
- transform: translateY(-2px);
-}`,
- javascript: `function handleAction() {
- const card = document.getElementById('interactiveCard');
-
- // Add a pulse effect
- card.style.animation = 'pulse 0.6s ease-in-out';
-
- // Show success message
- setTimeout(() => {
- alert('Action completed successfully!');
- card.style.animation = '';
- }, 600);
-}
-
-// Add CSS animation dynamically
-const style = document.createElement('style');
-style.textContent = \`
- @keyframes pulse {
- 0% { transform: scale(1); }
- 50% { transform: scale(1.05); }
- 100% { transform: scale(1); }
- }
-\`;
-document.head.appendChild(style);
-
-// Add particle effect on hover
-document.addEventListener('DOMContentLoaded', function() {
- const card = document.getElementById('interactiveCard');
-
- card.addEventListener('mouseenter', function() {
- createParticles();
- });
-});
-
-function createParticles() {
- const container = document.querySelector('.container');
-
- for (let i = 0; i < 6; i++) {
- const particle = document.createElement('div');
- particle.style.cssText = \`
- position: absolute;
- width: 4px;
- height: 4px;
- background: #4ade80;
- border-radius: 50%;
- pointer-events: none;
- animation: float 2s ease-out forwards;
- left: \${Math.random() * 100}%;
- top: \${Math.random() * 100}%;
- \`;
-
- container.appendChild(particle);
-
- setTimeout(() => {
- particle.remove();
- }, 2000);
- }
-}
-
-// Add float animation
-const floatStyle = document.createElement('style');
-floatStyle.textContent = \`
- @keyframes float {
- 0% {
- opacity: 1;
- transform: translateY(0px);
- }
- 100% {
- opacity: 0;
- transform: translateY(-50px);
- }
- }
-\`;
-document.head.appendChild(floatStyle);`,
-
+ html: `\n\n\n \n \n Landing Page \n\n\n \n Welcome to the Future Build amazing things
Get Started \n\n`,
+ css: `body{margin:0;font-family:'Segoe UI',sans-serif}.header{background:#130a2e;padding:1rem 2rem;position:fixed;width:100%;top:0;z-index:1000}.nav{display:flex;justify-content:space-between;align-items:center}.logo{color:white;font-size:1.5rem;font-weight:bold}.hero{height:100vh;background:#130a2e;display:flex;align-items:center;justify-content:center;text-align:center;color:white}.hero-content h1{font-size:4rem;margin-bottom:1rem}.hero-content p{color:#c5bedb;margin-bottom:2rem}.hero-content button{padding:1rem 2.5rem;border:none;border-radius:50px;cursor:pointer;font-weight:700;font-size:1.1rem}`,
+ javascript: `console.log('Landing page loaded!')`,
},
},
{
@@ -1080,6 +156,8 @@ document.head.appendChild(floatStyle);`,
},
]
+type LayoutType = "split" | "preview" | "code"
+
export default function CodeEditor() {
const [code, setCode] = useState(() => {
if (typeof window === "undefined") return templates[0].content
@@ -1088,20 +166,203 @@ export default function CodeEditor() {
const sharedCode = urlParams.get("code")
if (sharedCode) return JSON.parse(safeBase64Decode(sharedCode)) as CodeContent
} catch {
- // ignore invalid share URL
+ // invalid share URL — fall through
}
try {
const saved = localStorage.getItem("webify_code")
if (saved) return JSON.parse(saved) as CodeContent
} catch {
- // ignore corrupted local storage
+ // corrupted storage — fall through
}
return templates[0].content
})
+ const [layout, setLayout] = useState("split")
const [activeTab, setActiveTab] = useState("html")
+ const [isFullscreen, setIsFullscreen] = useState(false)
const [theme, setTheme] = useState<"light" | "dark">("light")
+ const [paletteOpen, setPaletteOpen] = useState(false)
+ const [autoRun, setAutoRun] = useState(true)
+ const [splitRatio, setSplitRatio] = useState(50)
+ const [isResizing, setIsResizing] = useState(false)
+
+
+ // use effect for handling full screen mode
+ useEffect(() => {
+ const handleFullscreenChange = () => {
+ setIsFullscreen(Boolean(document.fullscreenElement));
+ };
+
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
+ return () => {
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
+ };
+ }, []);
+
+ const handleFullscreenToggle = async () => {
+ try {
+ if (document.fullscreenElement) {
+ if (document.exitFullscreen) {
+ await document.exitFullscreen();
+ }
+ return;
+ }
+
+ if (isFullscreen) {
+ setIsFullscreen(false);
+ return;
+ }
+
+ if (containerRef.current?.requestFullscreen) {
+ await containerRef.current.requestFullscreen();
+ return;
+ }
+
+ setIsFullscreen(true);
+ } catch (err) {
+ console.error("Error attempting to toggle fullscreen:", err);
+ setIsFullscreen((prev) => !prev);
+ }
+ };
+
+const containerRef = useRef(null)
+const previewRef = useRef(null)
+const [isMobile, setIsMobile] = useState(false)
+
+useEffect(() => {
+ const updateIsMobile = () => {
+ setIsMobile(window.innerWidth < 768)
+ }
+
+ updateIsMobile()
+ window.addEventListener("resize", updateIsMobile)
+
+ return () => {
+ window.removeEventListener("resize", updateIsMobile)
+ }
+}, [])
+
+const handleDragStart = () => {
+ isDragging.current = true;
+ setIsResizing(true);
+ document.body.style.userSelect = "none";
+};
+
+const handleDragMove = useCallback((clientX: number, clientY: number) => {
+ if (!isDragging.current || !containerRef.current) return;
+
+ const rect = containerRef.current.getBoundingClientRect();
+
+ let newRatio;
+ if (isMobile) {
+ newRatio = ((clientY - rect.top) / rect.height) * 100;
+ } else {
+ newRatio = ((clientX - rect.left) / rect.width) * 100;
+ }
+
+ const clampedRatio = Math.max(20, Math.min(80, newRatio));
+ setSplitRatio(clampedRatio);
+}, [isMobile]);
+
+const handleMouseMove = useCallback((e: globalThis.MouseEvent) => {
+ handleDragMove(e.clientX, e.clientY);
+}, [handleDragMove]);
+
+const handleTouchMove = useCallback((e: globalThis.TouchEvent) => {
+ if (isDragging.current) {
+ handleDragMove(e.touches[0].clientX, e.touches[0].clientY);
+ }
+}, [handleDragMove]);
+
+const handleDragEnd = useCallback(() => {
+ isDragging.current = false;
+ setIsResizing(false);
+ document.body.style.userSelect = "auto";
+ document.body.style.cursor = "default";
+}, []);
+
+ // Tracks which template is currently active
+
+ const [isMobile, setIsMobile] = useState(false)
+ const [consoleErrors, setConsoleErrors] = useState>([])
+ const [runtimeError, setRuntimeError] = useState<{
+ message: string;
+ line: number | null;
+ column: number | null;
+ } | null>(null)
+ const [consoleOpen, setConsoleOpen] = useState(false)
+ const [moreSheetOpen, setMoreSheetOpen] = useState(false)
+
+ const [currentTemplateId, setCurrentTemplateId] = useState(null)
+ const [templateSnapshots, setTemplateSnapshots] = useState>(() => {
+ if (typeof window === "undefined") return {}
+ try {
+ const saved = localStorage.getItem("webify_template_snapshots")
+ if (saved) return JSON.parse(saved) as Record
+ } catch {
+ // corrupted storage — fall through
+ }
+ return {}
+ })
+
+ const isDragging = useRef(false)
+ const containerRef = useRef(null)
const previewRef = useRef(null)
+ const activeEditorRef = useRef(null)
+ const codeRef = useRef(code)
+
+ const htmlValidation = useMemo(() => validateHtmlSyntax(code.html), [code.html])
+
+ useEffect(() => { codeRef.current = code }, [code])
+
+ useEffect(() => {
+ const handleResize = () => setIsMobile(window.innerWidth < 768)
+ handleResize()
+ window.addEventListener("resize", handleResize)
+ return () => window.removeEventListener("resize", handleResize)
+ }, [])
+
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent) => {
+ if (event.data?.type === "WEBIFY_ERROR") {
+ setRuntimeError({
+ message: event.data.message,
+ line: event.data.line ?? null,
+ column: event.data.column ?? event.data.col ?? null,
+ })
+ setConsoleErrors((prev) => [...prev, {
+ message: event.data.message,
+ line: event.data.line,
+ col: event.data.col,
+ }])
+ setConsoleOpen(true)
+ }
+ }
+ window.addEventListener("message", handleMessage)
+ return () => window.removeEventListener("message", handleMessage)
+ }, [])
+
+ useEffect(() => {
+ const handleFullscreenChange = () => {
+ setIsFullscreen(!!document.fullscreenElement)
+ }
+ document.addEventListener("fullscreenchange", handleFullscreenChange)
+ return () => document.removeEventListener("fullscreenchange", handleFullscreenChange)
+ }, [])
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ try { localStorage.setItem('webify_code', JSON.stringify(code)) } catch {}
+ }, 500)
+ return () => clearTimeout(timer)
+ }, [code])
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ try { localStorage.setItem('webify_template_snapshots', JSON.stringify(templateSnapshots)) } catch {}
+ }, 500)
+ return () => clearTimeout(timer)
+ }, [templateSnapshots])
useEffect(() => {
const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null
@@ -1115,44 +376,134 @@ export default function CodeEditor() {
}
}, [])
+ const toggleTheme = () => {
+ if (theme === "light") {
+ setTheme("dark")
+ document.documentElement.classList.add("dark")
+ localStorage.setItem("theme", "dark")
+ } else {
+ setTheme("light")
+ document.documentElement.classList.remove("dark")
+ localStorage.setItem("theme", "light")
+ }
+ }
+
+ const handleDragStart = useCallback(() => {
+ isDragging.current = true
+ setIsResizing(true)
+ document.body.style.userSelect = "none"
+ }, [])
+
+ const handleDragMove = useCallback((clientX: number, clientY: number) => {
+ if (!isDragging.current || !containerRef.current) return
+ const rect = containerRef.current.getBoundingClientRect()
+ let newRatio: number
+ if (isMobile) {
+ newRatio = ((clientY - rect.top) / rect.height) * 100
+ } else {
+ newRatio = ((clientX - rect.left) / rect.width) * 100
+ }
+ setSplitRatio(Math.max(20, Math.min(80, newRatio)))
+ }, [isMobile])
+
+ const handleDragEnd = useCallback(() => {
+ isDragging.current = false
+ setIsResizing(false)
+ document.body.style.userSelect = "auto"
+ document.body.style.cursor = "default"
+ }, [])
+
+ const handleMouseMove = useCallback((e: globalThis.MouseEvent) => handleDragMove(e.clientX, e.clientY), [handleDragMove])
+ const handleTouchMove = useCallback((e: globalThis.TouchEvent) => {
+ if (isDragging.current) {
+ e.preventDefault()
+ handleDragMove(e.touches[0].clientX, e.touches[0].clientY)
+ }
+ }, [handleDragMove])
+
useEffect(() => {
- const timer = setTimeout(() => {
- try {
- localStorage.setItem("webify_code", JSON.stringify(code))
- } catch {
- // ignore storage quota errors
- }
- }, 300)
- return () => clearTimeout(timer)
- }, [code])
+ window.addEventListener("mousemove", handleMouseMove)
+ window.addEventListener("mouseup", handleDragEnd)
+ window.addEventListener("touchmove", handleTouchMove, { passive: false })
+ window.addEventListener("touchend", handleDragEnd)
+ return () => {
+ window.removeEventListener("mousemove", handleMouseMove)
+ window.removeEventListener("mouseup", handleDragEnd)
+ window.removeEventListener("touchmove", handleTouchMove)
+ window.removeEventListener("touchend", handleDragEnd)
+ }
+ }, [handleMouseMove, handleTouchMove, handleDragEnd])
useEffect(() => {
+ if (!previewRef.current || !autoRun) return
+ // clear runtime error on each recompile; timeout errors are caught by the iframe's own watchdog
+ setRuntimeError(null)
+ if (!htmlValidation.isValid) {
+ previewRef.current.srcdoc = createPreviewErrorHtml(htmlValidation.message ?? "Invalid HTML syntax.")
+ return
+ }
+ const debounceTimer = setTimeout(() => {
+ if (!previewRef.current) return
+ const combinedCode = `Live Preview ${code.html}Live Preview ${code.html}\n\n`)
zip.file("style.css", code.css)
zip.file("script.js", code.javascript)
const blob = await zip.generateAsync({ type: "blob" })
@@ -1162,91 +513,438 @@ export default function CodeEditor() {
a.download = "webify-project.zip"
document.body.appendChild(a)
a.click()
- a.remove()
+ document.body.removeChild(a)
URL.revokeObjectURL(url)
+ toast("Download started", { description: "Saved as webify-project.zip" })
+ }
+
+ const importCode = () => {
+ const input = document.createElement("input")
+ input.type = "file"
+ input.accept = ".html"
+ input.onchange = (e) => {
+ const file = (e.target as HTMLInputElement).files?.[0]
+ if (file) {
+ const reader = new FileReader()
+ reader.onload = (e) => {
+ const content = e.target?.result as string
+ const htmlMatch = content.match(/]*>([\s\S]*?)<\/body>/i)
+ const cssMatch = content.match(/