diff --git a/app/crypto/blockchain/page.tsx b/app/crypto/blockchain/page.tsx new file mode 100644 index 0000000..fc5f95d --- /dev/null +++ b/app/crypto/blockchain/page.tsx @@ -0,0 +1,5 @@ +import { BlockchainPage } from "@/components/crypto/blockchain-page"; + +export default function Page() { + return ; +} diff --git a/app/crypto/page.tsx b/app/crypto/page.tsx new file mode 100644 index 0000000..1434cf3 --- /dev/null +++ b/app/crypto/page.tsx @@ -0,0 +1,5 @@ +import { UuidPage } from "@/components/crypto/uuid-page"; + +export default function Page() { + return ; +} diff --git a/app/docs/page.tsx b/app/docs/page.tsx new file mode 100644 index 0000000..4a0351f --- /dev/null +++ b/app/docs/page.tsx @@ -0,0 +1,5 @@ +import { DocsPage } from "@/components/docs"; + +export default function Page() { + return ; +} diff --git a/app/documentation/page.tsx b/app/documentation/page.tsx index 68f70e7..a218bbf 100644 --- a/app/documentation/page.tsx +++ b/app/documentation/page.tsx @@ -1,130 +1,142 @@ "use client"; import React from 'react'; -import { - Book, - Zap, - Shield, - Globe, - Braces, - Box, - Terminal, - Cpu -} from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { HelpCircle, Book, Code, Sparkles, Terminal } from 'lucide-react'; +import Footer from "@/components/common/Footer"; -import { Footer } from "@/components/landing/footer"; - -const DOCS_SECTIONS = [ - { - title: "Getting Started", - items: [ - { - icon: Zap, - label: "Introduction", - content: "Web Utils is a high-performance, universal file editor and code previewer designed for modern developers. It allows you to instantly edit, visualize, and format multiple data structures with zero setup." - }, - { - icon: Terminal, - label: "Quick Start", - content: "Navigate to the /view route, paste your code into the editor, and watch as it instantly renders in the preview pane. Use the sidebar to switch between output formats." - } - ] - }, - { - title: "Supported Formats", - items: [ - { - icon: Globe, - label: "HTML & Bootstrap", - content: "Full support for HTML5 and Bootstrap 5.3. Renders code in a safe, sandboxed iframe environment." - }, - { - icon: Braces, - label: "JSON & YAML", - content: "Automatic syntax validation and beautification for structured data. Error highlighting for invalid syntax." - }, - { - icon: Box, - label: "React (JSX/TSX)", - content: "Premium syntax highlighting for React components. Perfect for reviewing component structures." - } - ] - }, - { - title: "Advanced Features", - items: [ - { - icon: Cpu, - label: "Performance", - content: "Built on Next.js 15 for lightning-fast route transitions and optimized rendering cycles." - }, - { - icon: Shield, - label: "Security", - content: "All previews are sandboxed to prevent script execution from impacting the main application window." - } - ] - } -]; - -export default function DocsPage() { +export default function DocumentationPage() { return ( -
-
-
-
- - Documentation -
-

- Everything you need to
scale your workflow. +
+
+ {/* Header */} +
+

+ + Documentation

-

- A comprehensive guide to using and extending the Web Utils platform. +

+ Learn how to use Web Utils to supercharge your developer workflow.

-

+
-
- {DOCS_SECTIONS.map((section) => ( -
-

- - {section.title} -

-
- {section.items.map((item) => ( -
-
- -
-

{item.label}

-

- {item.content} -

-
- ))} -
-
- ))} +
+ + + + Getting Started + + + +

+ Web Utils is a suite of carefully crafted utilities running entirely in your browser. No data is sent to external servers, ensuring complete privacy and security for your sensitive code and tokens. +

+

+ Simply select a tool from the sidebar to begin. +

+
+
+ + + + + Code Editors + + + +

+ All text-based tools use the Monaco Editor (the core of VS Code). This provides you with powerful features like: +

+
    +
  • Syntax highlighting & validation
  • +
  • Minimap for large files
  • +
  • Multiple cursors
  • +
  • Command Palette (F1)
  • +
+
+
+ + + + + Formatting + + + +

+ Need to format an ugly JSON or un-minified HTML block? Most editors have a "Format" button near the top right, or you can right-click anywhere in the editor and select "Format Document". +

+
+
+ + + + + Keyboard Shortcuts + + + +
    +
  • + Command Palette + Ctrl + K +
  • +
  • + Toggle Sidebar + Ctrl + B +
  • +
  • + Format Document + Shift + Alt + F +
  • +
+
+
- {/* Footer info */} -
-

Ready to get started?

-

- Join thousands of developers using Web Utils to edit and preview their code more efficiently. -

-
- - + {/* Learn Section */} +
+

+ + How Things Work +

+ +
+ + + + Blockchain Technology + + + +

+ At its core, a blockchain is a distributed ledger. Imagine a digital spreadsheet that is copied thousands of times across a network of computers. This network is designed to regularly update this spreadsheet. +

+
+
+
1. Blocks
+

Data is bundled into blocks. Each block has a unique hash and the hash of the previous block.

+
+
+
2. Hashing
+

A cryptographic fingerprint. If any data in the block changes, the hash changes completely.

+
+
+
3. Consensus
+

The network must agree on the validity of a block before it is added to the chain.

+
+
+

+ This structure makes the blockchain immutable—once a block is added, it cannot be changed without changing all subsequent blocks, which would require the consensus of the entire network. +

+
+
-
+
+
+
); } diff --git a/app/draw/page.tsx b/app/draw/page.tsx new file mode 100644 index 0000000..5b79415 --- /dev/null +++ b/app/draw/page.tsx @@ -0,0 +1,5 @@ +import { DrawPage } from "@/components/draw/draw-page"; + +export default function Page() { + return ; +} diff --git a/app/dummy/page.tsx b/app/dummy/page.tsx new file mode 100644 index 0000000..2a391d0 --- /dev/null +++ b/app/dummy/page.tsx @@ -0,0 +1,5 @@ +import { DummyFilePage } from "@/components/dummy/dummy-page"; + +export default function Page() { + return ; +} diff --git a/app/editor/page.tsx b/app/editor/page.tsx index 6fb6743..66a53ee 100644 --- a/app/editor/page.tsx +++ b/app/editor/page.tsx @@ -1,15 +1,14 @@ "use client"; -import { EditorContainer } from "@/components/editor/editor-container"; -import { DEFAULT_CONTENT } from "@/data/default-content"; +import { WorkspaceContainer } from "@/components/workspace/workspace-container"; export default function EditorPage() { return (
-
diff --git a/app/globals.css b/app/globals.css index 170a52e..4d0a255 100644 --- a/app/globals.css +++ b/app/globals.css @@ -135,18 +135,33 @@ } .custom-scrollbar::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.05); + background: rgba(0, 0, 0, 0.03); border-radius: 10px; } .dark .custom-scrollbar::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.05); + background: rgba(255, 255, 255, 0.03); } .custom-scrollbar:hover::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.1); + background: rgba(0, 0, 0, 0.08); } .dark .custom-scrollbar:hover::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.08); } + +/* Global Smooth Transitions */ +.smooth-layout { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Monaco Editor Overrides */ +.monaco-editor, .monaco-editor .margin, .monaco-editor-background { + background-color: transparent !important; +} + +.monaco-editor { + padding-top: 8px; +} + diff --git a/app/ide/page.tsx b/app/ide/page.tsx index 165d23b..d5ec12d 100644 --- a/app/ide/page.tsx +++ b/app/ide/page.tsx @@ -1,59 +1,11 @@ "use client"; -import React from 'react'; -import { Blocks, Rocket, Hammer, Construction } from 'lucide-react'; +import { WorkspaceContainer } from "@/components/workspace/workspace-container"; -export default function IDEPage() { +export default function IdePage() { return ( -
- {/* Background Decorative Elements */} -
-
- -
-
- - Under Development -
- -

- The Next-Gen
- Cloud IDE -

- -

- Building a browser-based client WASM type build IDE with real-time collaboration, - WASM-powered runtimes, and deep integration. -

- -
-
- -

Fast Runtime

-

WASM Powered

-
-
- -

Extensions

-

Rich Ecosystem

-
-
- -

Cloud Build

-

Instant Deploy

-
-
- -
- - -
-
- +
+
); } diff --git a/app/layout.tsx b/app/layout.tsx index db3e5c8..638866f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,8 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import type {Metadata} from "next"; +import {Geist, Geist_Mono} from "next/font/google"; import "./globals.css"; -import { ThemeProvider } from "@/components/layout/theme-provider"; -import { Navbar } from "@/components/layout/navbar"; +import {ThemeProvider} from "@/components/layout/theme-provider"; +import {ClientLayout} from "@/components/layout/client-layout"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -16,31 +16,30 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: "Web Utils | Universal Code Previewer", - description: "A professional tool for editing and previewing HTML, JSON, YAML, and React code with ease.", + description: + "A professional tool for editing and previewing HTML, JSON, YAML, and React code with ease.", }; export default function RootLayout({ children, -}: Readonly<{ + }: { children: React.ReactNode; -}>) { +}) { return ( - + -
- -
- {children} -
-
+ {/* CLIENT PART MOVED HERE */} + {children}
); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 92943c4..a562f13 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,15 +1,155 @@ "use client"; -import { Hero } from "@/components/landing/hero"; -import { Features } from "@/components/landing/features"; -import { Footer } from "@/components/landing/footer"; - -export default function Home() { - return ( -
- - -
-
- ); +import React, {useState, useEffect, useRef} from 'react'; +import {Search, Copy, Check} from 'lucide-react'; +import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; +import {AdsCard} from '@/components/shared/ads-card'; +import {type Category, type Tool, TOOL_CATEGORIES, TOOLS} from '@/lib/constants/tools'; +import {Card, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; +import Footer from "@/components/common/Footer"; + +export default function ToolsListingPage() { + const [searchQuery, setSearchQuery] = useState(""); + const [activeCategory, setActiveCategory] = useState("all"); + const [liveEpoch, setLiveEpoch] = useState(() => Math.floor(Date.now() / 1000)); + const [copied, setCopied] = useState(false); + const searchInputRef = useRef(null); + + useEffect(() => { + const interval = setInterval(() => { + setLiveEpoch(Math.floor(Date.now() / 1000)); + }, 1000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + searchInputRef.current?.focus(); + } + }; + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + + const filteredTools = TOOLS.filter((tool: Tool) => { + const matchesSearch = tool.name.toLowerCase().includes(searchQuery.toLowerCase()) || + tool.description.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesCategory = activeCategory === 'all' || tool.category === activeCategory; + return matchesSearch && matchesCategory; + }); + + const handleCopyEpoch = () => { + navigator.clipboard.writeText(String(liveEpoch)); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ {/* Header */} +
+
+

+ Developer Tools +

+

+ A clean, minimalist suite of developer utilities and converters. +

+
+ +
+
+
+ + {liveEpoch} + + + {new Date(liveEpoch * 1000).toUTCString().split(' ').slice(0, 5).join(' ')} UTC + +
+
+ {copied ? : } +
+
+
+ + {/* Search and Filters */} +
+
+ + setSearchQuery(e.target.value)} + /> +
+ + K + +
+
+
+ + {TOOL_CATEGORIES.map((cat: Category) => ( + + ))} +
+
+ + {/* Unified Tools Grid */} +
+ {filteredTools.map((tool: Tool) => ( + window.open(tool.href, '_self')} + > + +
+ +
+ {tool.name} + {tool.description} +
+ + {tool.status} + +
+
+
+ ))} +
+ +
+
+
+
+
+ ); } diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 0000000..f1b9dc1 --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,244 @@ +"use client"; + +import React from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Settings, Moon, Sun, Monitor, Type, Code, Clock, Globe, Timer } from 'lucide-react'; +import { useTheme } from 'next-themes'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import Footer from "@/components/common/Footer"; + +import { useLocalStorage } from '@/hooks/use-local-storage'; + +export default function SettingsPage() { + const { theme, setTheme } = useTheme(); + const [fontSize, setFontSize] = useLocalStorage('editorFontSize', 14); + const [tabSize, setTabSize] = useLocalStorage('editorTabSize', 4); + const [timeZone, setTimeZone] = useLocalStorage('timeZone', 'UTC'); + const [timeFormat, setTimeFormat] = useLocalStorage('timeFormat', 'seconds'); + + return ( +
+
+ {/* Header */} +
+

+ + Settings +

+

+ Manage your preferences, workspace configuration, and themes. +

+
+ +
+ {/* Appearance */} + + + Appearance + Customize the look and feel of Web Utils. + + +
+ + + +
+
+
+ + {/* Editor Preferences */} + + + Editor Configuration + Default settings for Monaco Editor workspaces. + + +
+ + setFontSize(Number(e.target.value))} + className="w-full bg-muted/50 border-transparent focus-visible:ring-indigo-500" + /> +
+ +
+ + setTabSize(Number(e.target.value))} + className="w-full bg-muted/50 border-transparent focus-visible:ring-indigo-500" + /> +
+
+
+ + {/* Time & Date Preferences */} + + + + + Time & Date Configuration + + Default preferences for epoch conversion and time display. + + +
+
+ +
+ + +
+

Sets the primary display format for human-readable dates.

+
+ +
+ +
+ + +
+

Default precision when using "Now" or generating timestamps.

+
+
+
+
+ + {/* Data Management */} + + + Data Management + Export or import your application settings and workspace data. + + + + +
+ { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + try { + const data = JSON.parse(event.target?.result as string); + Object.entries(data).forEach(([key, value]) => { + localStorage.setItem(key, value as string); + }); + window.location.reload(); + } catch { + alert('Invalid settings file'); + } + }; + reader.readAsText(file); + } + }} + /> + +
+ + +
+
+
+
+
+
+
+
+ ); +} diff --git a/app/time/page.tsx b/app/time/page.tsx new file mode 100644 index 0000000..d2653d7 --- /dev/null +++ b/app/time/page.tsx @@ -0,0 +1,5 @@ +import { TimePage } from "@/components/time"; + +export default function Page() { + return ; +} diff --git a/app/view/[type]/page.tsx b/app/view/[type]/page.tsx index 2685141..24657ce 100644 --- a/app/view/[type]/page.tsx +++ b/app/view/[type]/page.tsx @@ -1,8 +1,6 @@ -import { SidebarProvider } from "@/components/ui/sidebar"; -import { AppSidebar } from "@/components/layout/app-sidebar"; -import { ViewerContainer } from "@/components/view/viewer-container"; -import { Format } from "@/types"; -import { DEFAULT_CONTENT } from "@/data/default-content"; +import {ViewerContainer} from "@/components/view/viewer-container"; +import {Format} from "@/types"; +import {DEFAULT_CONTENT} from "@/data/default-content"; export function generateStaticParams() { return [ @@ -23,16 +21,13 @@ export default async function ViewPage({ params }: { params: Promise<{ type: str const content = (DEFAULT_CONTENT as Record)[format] || ""; return ( - -
- -
- -
-
-
+
+
+ +
+
); } diff --git a/app/view/page.tsx b/app/view/page.tsx index c93f5bf..5fba31b 100644 --- a/app/view/page.tsx +++ b/app/view/page.tsx @@ -1,22 +1,13 @@ -"use client"; - -import { SidebarProvider } from "@/components/ui/sidebar"; -import { AppSidebar } from "@/components/layout/app-sidebar"; import { ViewerContainer } from "@/components/view/viewer-container"; import { DEFAULT_CONTENT } from "@/data/default-content"; export default function ViewPage() { return ( - -
- -
- -
-
-
+
+ +
); } diff --git a/components/common/Footer.tsx b/components/common/Footer.tsx new file mode 100644 index 0000000..a245e3f --- /dev/null +++ b/components/common/Footer.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Github } from 'lucide-react'; +import Link from 'next/link'; + +export default function Footer() { + return ( +
+
+
+ © {new Date().getFullYear()} Web Utils +
+
+ Documentation + Privacy + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/components/common/Navbar.tsx b/components/common/Navbar.tsx new file mode 100644 index 0000000..bf48733 --- /dev/null +++ b/components/common/Navbar.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { Button } from "@/components/ui/button"; + +function Navbar() { + return ( + + ); +} + +export default Navbar; \ No newline at end of file diff --git a/components/crypto/blockchain-page.tsx b/components/crypto/blockchain-page.tsx new file mode 100644 index 0000000..51078c5 --- /dev/null +++ b/components/crypto/blockchain-page.tsx @@ -0,0 +1,226 @@ +"use client"; + +import React, { useState } from 'react'; +import { + Bitcoin, + Search, + ArrowUpRight, + ArrowDownLeft, + Blocks, + Wallet, + Activity, + ExternalLink, + Cpu, + Globe, + Zap +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; + +interface Transaction { + type: 'in' | 'out'; + id: string; + date: string; + amount: string; + status: string; +} + +interface SearchResult { + type: 'transaction' | 'address'; + hash: string; + balance: string; + totalReceived: string; + totalSent: string; + txCount: number; + transactions: Transaction[]; + value?: string; +} + +export function BlockchainPage() { + const [query, setQuery] = useState(''); + const [isSearching, setIsSearching] = useState(false); + const [result, setResult] = useState(null); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (!query.trim()) return; + + setIsSearching(true); + // Simulate API call + setTimeout(() => { + setResult({ + type: query.length > 42 ? 'transaction' : 'address', + hash: query, + balance: '1.245 BTC', + totalReceived: '4.567 BTC', + totalSent: '3.322 BTC', + txCount: 42, + transactions: [ + { type: 'in', id: 'f418...d2c1', date: '2 mins ago', amount: '+0.042 BTC', status: 'CONFIRMED' }, + { type: 'out', id: 'a12b...e34f', date: '1 hour ago', amount: '-0.015 BTC', status: 'CONFIRMED' }, + { type: 'in', id: 'c98d...b76a', date: '5 hours ago', amount: '+1.120 BTC', status: 'CONFIRMED' }, + ], + value: '$78,452.12' + }); + setIsSearching(false); + }, 800); + }; + + return ( +
+
+ {/* Header */} +
+
+

+ + Blockchain Explorer +

+

+ Real-time simulation of Bitcoin and Ethereum network data. +

+
+ +
+ + setQuery(e.target.value)} + /> + + +
+ + {!result && !isSearching && ( +
+ {[ + { label: 'Current Block', value: '835,214', icon: Blocks, color: 'text-indigo-500' }, + { label: 'Avg Fee', value: '12 sat/vB', icon: Zap, color: 'text-amber-500' }, + { label: 'Total Nodes', value: '14,231', icon: Globe, color: 'text-emerald-500' }, + { label: 'Hashrate', value: '612.4 EH/s', icon: Activity, color: 'text-rose-500' } + ].map((stat, i) => ( + + +
+ +
+
+
{stat.label}
+
{stat.value}
+
+
+
+ ))} +
+ )} + + {isSearching && ( +
+
+
Scanning Network...
+
+ )} + + {result && !isSearching && ( +
+ + +
+ + {result.type} + +
+ +
+
+ + {result.hash} + +
+ +
+
Final Balance
+
+ {result.balance} + {result.value} +
+
+ + + +
+
+
Received
+
{result.totalReceived}
+
+
+
Sent
+
{result.totalSent}
+
+
+
Txs
+
{result.txCount}
+
+
+ +
+
+ + + + + Recent Transactions + + + +
+ {result.transactions.map((tx, i) => ( +
+
+
+ {tx.type === 'in' ? : } +
+
+
{tx.id}
+
{tx.date}
+
+
+
+
{tx.amount}
+
{tx.status}
+
+
+ ))} +
+
+
+
+ )} + + + +
+ +
+
+

Real-time Simulation

+

+ This explorer provides a simulated environment for testing blockchain queries. + In a production environment, this would connect to the Bitcoin Core or Ethereum JSON-RPC APIs. +

+
+
+
+
+
+ ); +} diff --git a/components/crypto/uuid-page.tsx b/components/crypto/uuid-page.tsx new file mode 100644 index 0000000..7b31add --- /dev/null +++ b/components/crypto/uuid-page.tsx @@ -0,0 +1,138 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { + Shield, + RefreshCw, + Copy, + Check, + Settings2, + Zap, + Hash, + Binary +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; + +export function UuidPage() { + const [uuids, setUuids] = useState([]); + const [count, setCount] = useState(5); + const [copiedIndex, setCopiedIndex] = useState(null); + const hasInitialized = React.useRef(false); + + const generateUuids = React.useCallback(() => { + const newUuids = Array.from({ length: count }, () => { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + // Fallback for older browsers + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + }); + setUuids(newUuids); + }, [count]); + + useEffect(() => { + if (!hasInitialized.current) { + queueMicrotask(() => { + generateUuids(); + }); + hasInitialized.current = true; + } + }, [generateUuids]); + + const handleCopy = (text: string, index: number) => { + navigator.clipboard.writeText(text); + setCopiedIndex(index); + setTimeout(() => setCopiedIndex(null), 2000); + }; + + return ( +
+
+ {/* Header */} +
+
+

+ + UUID v4 Generator +

+

+ Generate cryptographically strong unique identifiers instantly. +

+
+ +
+ +
+ {/* Controls */} + + + + Configuration + + + +
+ +
+ {[1, 5, 10, 20, 50].map(n => ( + + ))} +
+
+ +
+
+ + V4 (Random) Algorithm +
+
+ + 128-bit identifiers +
+
+ + 36 characters total +
+
+
+
+ + {/* Results List */} + +
+ {uuids.map((uuid, i) => ( +
+ + {uuid} + + +
+ ))} +
+
+
+
+
+ ); +} diff --git a/components/docs/DocsPage.tsx b/components/docs/DocsPage.tsx new file mode 100644 index 0000000..8c62af0 --- /dev/null +++ b/components/docs/DocsPage.tsx @@ -0,0 +1,130 @@ +"use client"; + +import React from 'react'; +import { + Book, + Zap, + Shield, + Globe, + Braces, + Box, + Terminal, + Cpu +} from 'lucide-react'; + +import { Footer } from "@/components/landing/footer"; + +const DOCS_SECTIONS = [ + { + title: "Getting Started", + items: [ + { + icon: Zap, + label: "Introduction", + content: "Web Utils is a high-performance, universal file editor and code previewer designed for modern developers. It allows you to instantly edit, visualize, and format multiple data structures with zero setup." + }, + { + icon: Terminal, + label: "Quick Start", + content: "Navigate to the /view route, paste your code into the editor, and watch as it instantly renders in the preview pane. Use the sidebar to switch between output formats." + } + ] + }, + { + title: "Supported Formats", + items: [ + { + icon: Globe, + label: "HTML & Bootstrap", + content: "Full support for HTML5 and Bootstrap 5.3. Renders code in a safe, sandboxed iframe environment." + }, + { + icon: Braces, + label: "JSON & YAML", + content: "Automatic syntax validation and beautification for structured data. Error highlighting for invalid syntax." + }, + { + icon: Box, + label: "React (JSX/TSX)", + content: "Premium syntax highlighting for React components. Perfect for reviewing component structures." + } + ] + }, + { + title: "Advanced Features", + items: [ + { + icon: Cpu, + label: "Performance", + content: "Built on Next.js 15 for lightning-fast route transitions and optimized rendering cycles." + }, + { + icon: Shield, + label: "Security", + content: "All previews are sandboxed to prevent script execution from impacting the main application window." + } + ] + } +]; + +export function DocsPage() { + return ( +
+
+
+
+ < Book className="size-3" /> + Documentation +
+

+ Everything you need to
scale your workflow. +

+

+ A comprehensive guide to using and extending the Web Utils platform. +

+
+ +
+ {DOCS_SECTIONS.map((section) => ( +
+

+ + {section.title} +

+
+ {section.items.map((item) => ( +
+
+ +
+

{item.label}

+

+ {item.content} +

+
+ ))} +
+
+ ))} +
+ + {/* Footer info */} +
+

Ready to get started?

+

+ Join thousands of developers using Web Utils to edit and preview their code more efficiently. +

+
+ + +
+
+
+
+
+ ); +} diff --git a/components/docs/index.ts b/components/docs/index.ts new file mode 100644 index 0000000..bf3ef9b --- /dev/null +++ b/components/docs/index.ts @@ -0,0 +1 @@ +export * from "./DocsPage"; diff --git a/components/draw/action-menu.tsx b/components/draw/action-menu.tsx new file mode 100644 index 0000000..215e1f6 --- /dev/null +++ b/components/draw/action-menu.tsx @@ -0,0 +1,45 @@ +"use client"; + +import React from 'react'; +import { MoreHorizontal, Save, FileDown, Share2, Settings } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Separator } from '@/components/ui/separator'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; + +interface ActionMenuProps { + handleDownload: () => void; +} + +export function ActionMenu({ handleDownload }: ActionMenuProps) { + return ( +
+ + + + + + + Save to... + + + Export image... Ctrl+Shift+E + + + + Live collaboration... + + + Help & Settings + + + +
+ ); +} diff --git a/components/draw/draw-page.tsx b/components/draw/draw-page.tsx new file mode 100644 index 0000000..0752393 --- /dev/null +++ b/components/draw/draw-page.tsx @@ -0,0 +1,504 @@ +"use client"; + +import React, { useRef, useState, useEffect, useLayoutEffect, useCallback } from 'react'; +import Script from 'next/script'; + +import { Element, ElementType, RoughCanvas } from './types'; +import { Toolbar } from './toolbar'; +import { StylePanel } from './style-panel'; +import { ZoomControls } from './zoom-controls'; +import { ActionMenu } from './action-menu'; + +// Type-safe access to global roughjs +interface RoughWindow extends Window { + rough: { + canvas: (canvas: HTMLCanvasElement) => RoughCanvas; + } +} + +// Define drawElement outside to ensure it's not re-created and is hoisted +function drawElement(rc: RoughCanvas, ctx: CanvasRenderingContext2D, element: Element) { + const generator = rc.generator; + const options = { + stroke: element.color, + strokeWidth: element.strokeWidth, + roughness: 1.2, + bowing: 1.5, + seed: element.id + 1, + }; + + switch (element.type) { + case 'line': rc.draw(generator.line(element.x1, element.y1, element.x2, element.y2, options)); break; + case 'rectangle': rc.draw(generator.rectangle(element.x1, element.y1, element.x2 - element.x1, element.y2 - element.y1, options)); break; + case 'diamond': { + const midX = (element.x1 + element.x2) / 2; + const midY = (element.y1 + element.y2) / 2; + rc.draw(generator.polygon([[midX, element.y1], [element.x2, midY], [midX, element.y2], [element.x1, midY]], options)); + break; + } + case 'circle': { + const width = element.x2 - element.x1; + const height = element.y2 - element.y1; + rc.draw(generator.ellipse(element.x1 + width / 2, element.y1 + height / 2, width, height, options)); + break; + } + case 'arrow': { + const headlen = 15; + const angle = Math.atan2(element.y2 - element.y1, element.x2 - element.x1); + rc.draw(generator.line(element.x1, element.y1, element.x2, element.y2, options)); + rc.draw(generator.line(element.x2, element.y2, element.x2 - headlen * Math.cos(angle - Math.PI / 6), element.y2 - headlen * Math.sin(angle - Math.PI / 6), options)); + rc.draw(generator.line(element.x2, element.y2, element.x2 - headlen * Math.cos(angle + Math.PI / 6), element.y2 - headlen * Math.sin(angle + Math.PI / 6), options)); + break; + } + case 'freehand': + if (element.points && element.points.length > 1) { + const stroke = element.points.map(p => [p.x, p.y] as [number, number]); + rc.draw(generator.curve(stroke, options)); + } + break; + case 'text': + ctx.font = `${32 * element.strokeWidth}px 'Outfit', sans-serif`; + ctx.fillStyle = element.color; + ctx.textBaseline = 'top'; + const lines = (element.text || '').split('\n'); + lines.forEach((line, i) => { + ctx.fillText(line, element.x1, element.y1 + i * (38 * element.strokeWidth)); + }); + break; + } +} + +export function DrawPage() { + const canvasRef = useRef(null); + const [elements, setElements] = useState([]); + const [action, setAction] = useState<'none' | 'drawing' | 'moving' | 'resizing' | 'panning' | 'selecting'>('none'); + const [tool, setTool] = useState('freehand'); + const [color, setColor] = useState('#1e1e1e'); + const [strokeWidth, setStrokeWidth] = useState(2); + const [roughCanvas, setRoughCanvas] = useState(null); + const [history, setHistory] = useState([]); + const [redoStack, setRedoStack] = useState([]); + const [selectedElementIds, setSelectedElementIds] = useState([]); + const [offset, setOffset] = useState({ x: 0, y: 0 }); + const [scale, setScale] = useState(1); + const [startPanPos, setStartPanPos] = useState({ x: 0, y: 0 }); + const [selectionBox, setSelectionBox] = useState<{ x1: number, y1: number, x2: number, y2: number } | null>(null); + const [isLocked, setIsLocked] = useState(false); + const [editingElement, setEditingElement] = useState(null); + + const getMousePos = useCallback((e: React.MouseEvent | MouseEvent) => { + if (!canvasRef.current) return { x: 0, y: 0 }; + const rect = canvasRef.current.getBoundingClientRect(); + return { + x: (e.clientX - rect.left - offset.x) / scale, + y: (e.clientY - rect.top - offset.y) / scale + }; + }, [offset, scale]); + + const onScriptLoad = () => { + if (typeof window !== 'undefined') { + const roughWindow = window as unknown as RoughWindow; + if (roughWindow.rough) { + const canvas = canvasRef.current; + if (canvas) setRoughCanvas(roughWindow.rough.canvas(canvas)); + } + } + }; + + useEffect(() => { + if (typeof window !== 'undefined') { + const roughWindow = window as unknown as RoughWindow; + if (roughWindow.rough && canvasRef.current && !roughCanvas) { + setRoughCanvas(roughWindow.rough.canvas(canvasRef.current)); + } + } + }, [roughCanvas]); + + useLayoutEffect(() => { + const canvas = canvasRef.current; + if (!canvas || !roughCanvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.save(); + ctx.translate(offset.x, offset.y); + ctx.scale(scale, scale); + + const sortedElements = [...elements].sort((a, b) => a.zIndex - b.zIndex); + sortedElements.forEach((element) => { + drawElement(roughCanvas, ctx, element); + + if (selectedElementIds.includes(element.id)) { + let minX = Math.min(element.x1, element.x2); + let maxX = Math.max(element.x1, element.x2); + let minY = Math.min(element.y1, element.y2); + let maxY = Math.max(element.y1, element.y2); + + if (element.type === 'freehand' && element.points && element.points.length > 0) { + minX = Math.min(...element.points.map(p => p.x)); + maxX = Math.max(...element.points.map(p => p.x)); + minY = Math.min(...element.points.map(p => p.y)); + maxY = Math.max(...element.points.map(p => p.y)); + } + + ctx.strokeStyle = '#3b82f6'; + ctx.setLineDash([5, 5]); + ctx.lineWidth = 1; + ctx.strokeRect(minX - 5, minY - 5, (maxX - minX) + 10, (maxY - minY) + 10); + ctx.setLineDash([]); + + // Draw handles + ctx.fillStyle = 'white'; + ctx.strokeStyle = '#3b82f6'; + ctx.lineWidth = 1.5; + const handleSize = 8 / scale; + + [ + [minX, minY], [maxX, minY], [minX, maxY], [maxX, maxY], + [(minX + maxX) / 2, minY], [(minX + maxX) / 2, maxY], + [minX, (minY + maxY) / 2], [maxX, (minY + maxY) / 2] + ].forEach(([x, y]) => { + ctx.beginPath(); + ctx.rect(x - handleSize / 2, y - handleSize / 2, handleSize, handleSize); + ctx.fill(); + ctx.stroke(); + }); + } + }); + + if (selectionBox) { + ctx.strokeStyle = '#3b82f6'; + ctx.lineWidth = 1 / scale; + ctx.strokeRect( + selectionBox.x1, + selectionBox.y1, + selectionBox.x2 - selectionBox.x1, + selectionBox.y2 - selectionBox.y1 + ); + ctx.fillStyle = 'rgba(59, 130, 246, 0.1)'; + ctx.fillRect( + selectionBox.x1, + selectionBox.y1, + selectionBox.x2 - selectionBox.x1, + selectionBox.y2 - selectionBox.y1 + ); + } + + ctx.restore(); + }, [elements, roughCanvas, selectedElementIds, selectionBox, offset, scale]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + if (selectedElementIds.length > 0 && !editingElement) { + setHistory(prev => [...prev, elements]); + setElements(prev => prev.filter(el => !selectedElementIds.includes(el.id))); + setSelectedElementIds([]); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [selectedElementIds, elements, editingElement]); + + const handleMouseDown = (e: React.MouseEvent) => { + if (isLocked) return; + const { x, y } = getMousePos(e); + + if (tool === 'hand') { + setAction('panning'); + setStartPanPos({ x: e.clientX, y: e.clientY }); + return; + } + + if (tool === 'selection') { + // Check if clicking on an element + const clickedElement = [...elements].reverse().find(el => { + const minX = Math.min(el.x1, el.x2) - 5; + const maxX = Math.max(el.x1, el.x2) + 5; + const minY = Math.min(el.y1, el.y2) - 5; + const maxY = Math.max(el.y1, el.y2) + 5; + return x >= minX && x <= maxX && y >= minY && y <= maxY; + }); + + if (clickedElement) { + if (e.shiftKey) { + setSelectedElementIds(prev => + prev.includes(clickedElement.id) + ? prev.filter(id => id !== clickedElement.id) + : [...prev, clickedElement.id] + ); + } else if (!selectedElementIds.includes(clickedElement.id)) { + setSelectedElementIds([clickedElement.id]); + } + setAction('moving'); + setStartPanPos({ x: x, y: y }); + } else { + setSelectedElementIds([]); + setAction('selecting'); + setSelectionBox({ x1: x, y1: y, x2: x, y2: y }); + } + return; + } + + setAction('drawing'); + const id = Date.now(); + const newElement: Element = { + id, + type: tool, + x1: x, + y1: y, + x2: x, + y2: y, + color, + strokeWidth, + zIndex: elements.length, + points: tool === 'freehand' ? [{ x, y }] : undefined, + text: tool === 'text' ? '' : undefined + }; + setElements(prev => [...prev, newElement]); + setHistory(prev => [...prev, elements]); + if (tool === 'text') { + setEditingElement(newElement); + setAction('none'); + } + }; + + const handleMouseMove = (e: React.MouseEvent) => { + const { x, y } = getMousePos(e); + + if (action === 'panning') { + const dx = e.clientX - startPanPos.x; + const dy = e.clientY - startPanPos.y; + setOffset(prev => ({ x: prev.x + dx, y: prev.y + dy })); + setStartPanPos({ x: e.clientX, y: e.clientY }); + return; + } + + if (action === 'drawing') { + const index = elements.length - 1; + const updatedElements = [...elements]; + const element = updatedElements[index]; + element.x2 = x; + element.y2 = y; + if (element.type === 'freehand' && element.points) { + element.points.push({ x, y }); + } + setElements(updatedElements); + } else if (action === 'moving') { + const dx = x - startPanPos.x; + const dy = y - startPanPos.y; + setElements(prev => prev.map(el => { + if (selectedElementIds.includes(el.id)) { + const newEl = { ...el, x1: el.x1 + dx, y1: el.y1 + dy, x2: el.x2 + dx, y2: el.y2 + dy }; + if (newEl.type === 'freehand' && newEl.points) { + newEl.points = newEl.points.map(p => ({ x: p.x + dx, y: p.y + dy })); + } + return newEl; + } + return el; + })); + setStartPanPos({ x, y }); + } else if (action === 'selecting' && selectionBox) { + setSelectionBox({ ...selectionBox, x2: x, y2: y }); + } + }; + + const handleDoubleClick = (e: React.MouseEvent) => { + const { x, y } = getMousePos(e); + const clickedElement = [...elements].reverse().find(el => { + const minX = Math.min(el.x1, el.x2) - 5; + const maxX = Math.max(el.x1, el.x2) + 5; + const minY = Math.min(el.y1, el.y2) - 5; + const maxY = Math.max(el.y1, el.y2) + 5; + return x >= minX && x <= maxX && y >= minY && y <= maxY; + }); + + if (clickedElement && clickedElement.type === 'text') { + setEditingElement(clickedElement); + } else if (!clickedElement) { + // Create new text element + const id = Date.now(); + const newElement: Element = { + id, + type: 'text', + x1: x, + y1: y, + x2: x, + y2: y, + color, + strokeWidth, + zIndex: elements.length, + text: '' + }; + setElements(prev => [...prev, newElement]); + setEditingElement(newElement); + } + }; + + const deleteSelected = useCallback(() => { + if (selectedElementIds.length > 0) { + setHistory(prev => [...prev, elements]); + setElements(prev => prev.filter(el => !selectedElementIds.includes(el.id))); + setSelectedElementIds([]); + } + }, [selectedElementIds, elements]); + + const handleMouseUp = () => { + if (action === 'selecting' && selectionBox) { + const minX = Math.min(selectionBox.x1, selectionBox.x2); + const maxX = Math.max(selectionBox.x1, selectionBox.x2); + const minY = Math.min(selectionBox.y1, selectionBox.y2); + const maxY = Math.max(selectionBox.y1, selectionBox.y2); + + const newlySelected = elements.filter(el => { + const elMinX = Math.min(el.x1, el.x2); + const elMaxX = Math.max(el.x1, el.x2); + const elMinY = Math.min(el.y1, el.y2); + const elMaxY = Math.max(el.y1, el.y2); + return elMinX >= minX && elMaxX <= maxX && elMinY >= minY && elMaxY <= maxY; + }); + setSelectedElementIds(newlySelected.map(el => el.id)); + } + setAction('none'); + setSelectionBox(null); + }; + + const handleWheel = (e: React.WheelEvent) => { + if (e.ctrlKey || e.shiftKey) { + e.preventDefault(); + const delta = e.deltaY > 0 ? 0.9 : 1.1; + setScale(prev => Math.max(0.1, Math.min(10, prev * delta))); + } else { + setOffset(prev => ({ x: prev.x - e.deltaX, y: prev.y - e.deltaY })); + } + }; + + const handleUndo = () => { if (history.length) { setRedoStack(p => [...p, elements]); setElements(history[history.length - 1]); setHistory(h => h.slice(0, -1)); } }; + const handleRedo = () => { if (redoStack.length) { setHistory(h => [...h, elements]); setElements(redoStack[redoStack.length - 1]); setRedoStack(r => r.slice(0, -1)); } }; + const handleDownload = () => { const canvas = canvasRef.current; if (canvas) { const a = document.createElement('a'); a.download = 'sketch.png'; a.href = canvas.toDataURL(); a.click(); } }; + + const updateElement = (id: number, updates: Partial) => { + setElements(prev => prev.map(el => el.id === id ? { ...el, ...updates } : el)); + }; + + const bringToFront = () => { + const maxZ = Math.max(...elements.map(e => e.zIndex), 0); + setElements(prev => prev.map(e => selectedElementIds.includes(e.id) ? { ...e, zIndex: maxZ + 1 } : e)); + }; + + const sendToBack = () => { + const minZ = Math.min(...elements.map(e => e.zIndex), 0); + setElements(prev => prev.map(e => selectedElementIds.includes(e.id) ? { ...e, zIndex: minZ - 1 } : e)); + }; + + return ( +
+ ' : ''} @@ -43,7 +43,7 @@ export function HTMLViewer({ content, useBootstrap = true, useTailwind = true, e return (