Spaces:
Build error
Build error
| import { useState, useEffect, useRef } from 'react' | |
| import { FiPlay, FiSave, FiShare2, FiSettings, FiDownload, FiUpload, FiTerminal, FiDebug, FiGitPullRequest, FiSearch, FiFile, FiFolder } from 'react-icons/fi' | |
| import dynamic from 'next/dynamic' | |
| import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' | |
| import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' | |
| import { useHotkeys } from 'react-hotkeys-hook' | |
| const MonacoEditor = dynamic(() => import('@monaco-editor/react'), { | |
| ssr: false, | |
| loading: () => <div className="h-full bg-editor rounded-lg animate-pulse" /> | |
| }) | |
| export default function Editor() { | |
| const [code, setCode] = useState('') | |
| const [language, setLanguage] = useState('javascript') | |
| const [output, setOutput] = useState('') | |
| const [isRunning, setIsRunning] = useState(false) | |
| const [error, setError] = useState(null) | |
| const [theme, setTheme] = useState('vs-dark') | |
| const [fontSize, setFontSize] = useState(14) | |
| const [showSettings, setShowSettings] = useState(false) | |
| const [savedSnippets, setSavedSnippets] = useState([]) | |
| const [snippetName, setSnippetName] = useState('') | |
| const [activeTab, setActiveTab] = useState('editor') | |
| const [terminalOutput, setTerminalOutput] = useState('') | |
| const [debugMode, setDebugMode] = useState(false) | |
| const editorRef = useRef(null) | |
| const languages = [ | |
| { value: 'javascript', label: 'JavaScript', icon: '🟡' }, | |
| { value: 'typescript', label: 'TypeScript', icon: '🔵' }, | |
| { value: 'python', label: 'Python', icon: '🐍' }, | |
| { value: 'java', label: 'Java', icon: '☕' }, | |
| { value: 'csharp', label: 'C#', icon: '🔧' }, | |
| { value: 'go', label: 'Go', icon: '🐹' }, | |
| { value: 'rust', label: 'Rust', icon: '🦀' }, | |
| { value: 'html', label: 'HTML', icon: '📄' }, | |
| { value: 'css', label: 'CSS', icon: '🎨' } | |
| ] | |
| const themes = [ | |
| { value: 'vs-dark', label: 'Dark (VSCode)' }, | |
| { value: 'vs-light', label: 'Light (VSCode)' }, | |
| { value: 'hc-black', label: 'High Contrast' } | |
| ] | |
| useEffect(() => { | |
| const saved = localStorage.getItem('codeSnippets') | |
| if (saved) { | |
| setSavedSnippets(JSON.parse(saved)) | |
| } | |
| }, []) | |
| const handleRunCode = async () => { | |
| if (!code.trim()) return | |
| setIsRunning(true) | |
| setError(null) | |
| setOutput('') | |
| setTerminalOutput('Running code...\n') | |
| try { | |
| const response = await fetch('/api/execute', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ code, language }), | |
| }) | |
| const data = await response.json() | |
| if (response.ok) { | |
| setOutput(data.output) | |
| setTerminalOutput(prev => prev + `\nExecution completed successfully.\nOutput:\n${data.output}`) | |
| } else { | |
| throw new Error(data.error || 'Execution failed') | |
| } | |
| } catch (err) { | |
| setError(err.message) | |
| setTerminalOutput(prev => prev + `\nError: ${err.message}`) | |
| } finally { | |
| setIsRunning(false) | |
| } | |
| } | |
| const handleSaveSnippet = () => { | |
| if (!snippetName.trim()) return | |
| const newSnippet = { | |
| id: Date.now(), | |
| name: snippetName, | |
| code, | |
| language, | |
| createdAt: new Date().toISOString() | |
| } | |
| const updatedSnippets = [...savedSnippets, newSnippet] | |
| setSavedSnippets(updatedSnippets) | |
| localStorage.setItem('codeSnippets', JSON.stringify(updatedSnippets)) | |
| setSnippetName('') | |
| } | |
| const handleLoadSnippet = (snippet) => { | |
| setCode(snippet.code) | |
| setLanguage(snippet.language) | |
| } | |
| const handleDownloadCode = () => { | |
| const blob = new Blob([code], { type: 'text/plain' }) | |
| const url = URL.createObjectURL(blob) | |
| const a = document.createElement('a') | |
| a.href = url | |
| a.download = `code.${language === 'javascript' ? 'js' : language === 'python' ? 'py' : 'txt'}` | |
| document.body.appendChild(a) | |
| a.click() | |
| document.body.removeChild(a) | |
| URL.revokeObjectURL(url) | |
| } | |
| const handleUploadCode = (e) => { | |
| const file = e.target.files[0] | |
| if (!file) return | |
| const reader = new FileReader() | |
| reader.onload = (event) => { | |
| setCode(event.target.result) | |
| } | |
| reader.readAsText(file) | |
| } | |
| const handleEditorMount = (editor) => { | |
| editorRef.current = editor | |
| } | |
| useHotkeys('ctrl+s', (e) => { | |
| e.preventDefault() | |
| handleSaveSnippet() | |
| }) | |
| useHotkeys('f5', (e) => { | |
| e.preventDefault() | |
| handleRunCode() | |
| }) | |
| return ( | |
| <div className="flex flex-col h-[calc(100vh-64px)]"> | |
| <div className="flex items-center justify-between p-2 bg-panel border-b border-border"> | |
| <div className="flex items-center space-x-2"> | |
| <button | |
| onClick={() => setActiveTab('editor')} | |
| className={`px-3 py-1 rounded text-sm flex items-center space-x-1 ${activeTab === 'editor' ? 'bg-primary text-white' : 'text-gray-400 hover:bg-panel'}`} | |
| > | |
| <FiFile /> | |
| <span>Editor</span> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('terminal')} | |
| className={`px-3 py-1 rounded text-sm flex items-center space-x-1 ${activeTab === 'terminal' ? 'bg-primary text-white' : 'text-gray-400 hover:bg-panel'}`} | |
| > | |
| <FiTerminal /> | |
| <span>Terminal</span> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('debug')} | |
| className={`px-3 py-1 rounded text-sm flex items-center space-x-1 ${activeTab === 'debug' ? 'bg-primary text-white' : 'text-gray-400 hover:bg-panel'}`} | |
| > | |
| <FiDebug /> | |
| <span>Debug</span> | |
| </button> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <select | |
| value={language} | |
| onChange={(e) => setLanguage(e.target.value)} | |
| className="bg-editor text-white px-3 py-1 rounded text-sm" | |
| > | |
| {languages.map((lang) => ( | |
| <option key={lang.value} value={lang.value}> | |
| {lang.icon} {lang.label} | |
| </option> | |
| ))} | |
| </select> | |
| <button | |
| onClick={handleRunCode} | |
| disabled={isRunning} | |
| className={`px-3 py-1 rounded text-sm flex items-center space-x-1 ${isRunning ? 'bg-gray-600' : 'bg-success hover:bg-green-600'} text-white transition-colors`} | |
| > | |
| <FiPlay /> | |
| <span>{isRunning ? 'Running...' : 'Run (F5)'}</span> | |
| </button> | |
| <button | |
| onClick={() => setShowSettings(!showSettings)} | |
| className="px-3 py-1 bg-panel hover:bg-gray-600 text-white rounded text-sm flex items-center space-x-1 transition-colors" | |
| > | |
| <FiSettings /> | |
| <span>Settings</span> | |
| </button> | |
| </div> | |
| </div> | |
| {showSettings && ( | |
| <div className="bg-panel rounded-lg p-4 m-2 border border-border"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-300 mb-1">Theme</label> | |
| <select | |
| value={theme} | |
| onChange={(e) => setTheme(e.target.value)} | |
| className="w-full bg-editor text-white px-3 py-2 rounded" | |
| > | |
| {themes.map((t) => ( | |
| <option key={t.value} value={t.value}>{t.label}</option> | |
| ))} | |
| </select> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-300 mb-1">Font Size</label> | |
| <input | |
| type="range" | |
| min="12" | |
| max="24" | |
| value={fontSize} | |
| onChange={(e) => setFontSize(parseInt(e.target.value))} | |
| className="w-full" | |
| /> | |
| <div className="text-center text-sm text-gray-300 mt-1">{fontSize}px</div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div className="flex-1 overflow-hidden"> | |
| {activeTab === 'editor' && ( | |
| <div className="h-full bg-editor"> | |
| <MonacoEditor | |
| height="100%" | |
| language={language} | |
| theme={theme} | |
| value={code} | |
| onChange={setCode} | |
| onMount={handleEditorMount} | |
| options={{ | |
| fontSize, | |
| minimap: { enabled: true }, | |
| wordWrap: 'on', | |
| scrollBeyondLastLine: false, | |
| automaticLayout: true, | |
| renderLineHighlight: 'all', | |
| folding: true, | |
| lineNumbers: 'on', | |
| roundedSelection: true, | |
| scrollbar: { | |
| vertical: 'auto', | |
| horizontal: 'auto' | |
| } | |
| /> | |
| </div> | |
| )} | |
| {activeTab === 'terminal' && ( | |
| <div className="h-full bg-background p-4 font-mono text-sm text-gray-300 overflow-auto"> | |
| <div className="mb-2 flex items-center space-x-2"> | |
| <span className="text-green-400">$</span> | |
| <span>Terminal</span> | |
| </div> | |
| <div className="h-[calc(100%-30px)] overflow-auto"> | |
| {terminalOutput || 'Terminal output will appear here...'} | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'debug' && ( | |
| <div className="h-full bg-background p-4"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <h3 className="text-gray-300">Debug Console</h3> | |
| <button | |
| onClick={() => setDebugMode(!debugMode)} | |
| className={`px-3 py-1 rounded text-sm ${debugMode ? 'bg-danger' : 'bg-primary'} text-white`} | |
| > | |
| {debugMode ? 'Stop Debugging' : 'Start Debugging'} | |
| </button> | |
| </div> | |
| <div className="h-[calc(100%-50px)] bg-panel rounded p-3 overflow-auto"> | |
| {debugMode ? ( | |
| <div className="text-gray-300"> | |
| <p>Debug mode active. Breakpoints and variables will appear here.</p> | |
| <div className="mt-4 space-y-2"> | |
| <div className="flex justify-between"> | |
| <span>Status:</span> | |
| <span className="text-green-400">Running</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span>Breakpoints:</span> | |
| <span>0</span> | |
| </div> | |
| </div> | |
| </div> | |
| ) : ( | |
| <p className="text-gray-500">Debug console ready. Click "Start Debugging" to begin.</p> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="bg-panel border-t border-border p-2"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-2"> | |
| <input | |
| type="text" | |
| placeholder="Snippet name" | |
| value={snippetName} | |
| onChange={(e) => setSnippetName(e.target.value)} | |
| className="px-3 py-1 bg-editor text-white rounded text-sm w-48" | |
| /> | |
| <button | |
| onClick={handleSaveSnippet} | |
| disabled={!snippetName.trim()} | |
| className="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 text-white rounded text-sm flex items-center space-x-1 transition-colors disabled:opacity-50" | |
| > | |
| <FiSave /> | |
| <span>Save (Ctrl+S)</span> | |
| </button> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <label className="px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-sm cursor-pointer flex items-center space-x-1 transition-colors"> | |
| <FiUpload /> | |
| <span>Upload</span> | |
| <input type="file" className="hidden" onChange={handleUploadCode} accept=".js,.py,.java,.cs,.ts,.go,.rs,.html,.css" /> | |
| </label> | |
| <button | |
| onClick={handleDownloadCode} | |
| className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm flex items-center space-x-1 transition-colors" | |
| > | |
| <FiDownload /> | |
| <span>Download</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } |