Spaces:
Build error
Build error
| import { useState, useEffect, useRef } from 'react' | |
| import { FiPlay, FiSave, FiShare2, FiSettings, FiDownload, FiUpload } from 'react-icons/fi' | |
| import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' | |
| import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' | |
| import dynamic from 'next/dynamic' | |
| const MonacoEditor = dynamic(() => import('@monaco-editor/react'), { | |
| ssr: false, | |
| loading: () => <div className="h-full bg-gray-800 rounded-lg animate-pulse" /> | |
| }) | |
| export default function CodeEditor() { | |
| 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 editorRef = useRef(null) | |
| const languages = [ | |
| { value: 'javascript', label: 'JavaScript', icon: '🟡' }, | |
| { value: 'python', label: 'Python', icon: '🐍' }, | |
| { value: 'java', label: 'Java', icon: '☕' }, | |
| { value: 'csharp', label: 'C#', icon: '🔧' }, | |
| { value: 'typescript', label: 'TypeScript', icon: '🔵' }, | |
| { value: 'go', label: 'Go', icon: '🐹' }, | |
| { value: 'rust', label: 'Rust', icon: '🦀' } | |
| ] | |
| const themes = [ | |
| { value: 'vs-dark', label: 'Dark' }, | |
| { value: 'vs-light', label: 'Light' }, | |
| { 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('') | |
| 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) | |
| } else { | |
| throw new Error(data.error || 'Execution failed') | |
| } | |
| } catch (err) { | |
| setError(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 | |
| } | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex flex-wrap justify-between items-center gap-4"> | |
| <div className="flex items-center space-x-4"> | |
| <select | |
| value={language} | |
| onChange={(e) => setLanguage(e.target.value)} | |
| className="bg-gray-700 text-white px-3 py-2 rounded-lg" | |
| > | |
| {languages.map((lang) => ( | |
| <option key={lang.value} value={lang.value}> | |
| {lang.icon} {lang.label} | |
| </option> | |
| ))} | |
| </select> | |
| <button | |
| onClick={handleRunCode} | |
| disabled={isRunning} | |
| className={`px-4 py-2 rounded-lg flex items-center space-x-2 ${isRunning ? 'bg-gray-600' : 'bg-accent hover:bg-purple-600'} text-white transition-colors`} | |
| > | |
| <FiPlay /> | |
| <span>{isRunning ? 'Running...' : 'Run Code'}</span> | |
| </button> | |
| <button | |
| onClick={() => setShowSettings(!showSettings)} | |
| className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg flex items-center space-x-2 transition-colors" | |
| > | |
| <FiSettings /> | |
| <span>Settings</span> | |
| </button> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <label className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg cursor-pointer flex items-center space-x-2 transition-colors"> | |
| <FiUpload /> | |
| <span>Upload</span> | |
| <input type="file" className="hidden" onChange={handleUploadCode} accept=".js,.py,.java,.cs,.ts,.go,.rs" /> | |
| </label> | |
| <button | |
| onClick={handleDownloadCode} | |
| className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center space-x-2 transition-colors" | |
| > | |
| <FiDownload /> | |
| <span>Download</span> | |
| </button> | |
| <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-2 bg-gray-700 text-white rounded-lg text-sm" | |
| /> | |
| <button | |
| onClick={handleSaveSnippet} | |
| disabled={!snippetName.trim()} | |
| className="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg flex items-center space-x-2 transition-colors disabled:opacity-50" | |
| > | |
| <FiSave /> | |
| <span>Save</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {showSettings && ( | |
| <div className="bg-gray-800 rounded-lg p-4 mb-4"> | |
| <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-gray-700 text-white px-3 py-2 rounded-lg" | |
| > | |
| {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="grid grid-cols-1 lg:grid-cols-2 gap-6 h-[calc(100vh-250px)]"> | |
| <div className="bg-gray-800 rounded-lg overflow-hidden"> | |
| <div className="bg-gray-900 px-4 py-2 flex justify-between items-center"> | |
| <span className="text-sm text-gray-300">Editor</span> | |
| {savedSnippets.length > 0 && ( | |
| <select | |
| onChange={(e) => handleLoadSnippet(JSON.parse(e.target.value))} | |
| className="bg-gray-700 text-white px-2 py-1 rounded text-sm" | |
| > | |
| <option value="">Load Snippet</option> | |
| {savedSnippets.map((snippet) => ( | |
| <option key={snippet.id} value={JSON.stringify(snippet)}> | |
| {snippet.name} ({snippet.language}) | |
| </option> | |
| ))} | |
| </select> | |
| )} | |
| </div> | |
| <div className="h-[calc(100%-40px)]"> | |
| <MonacoEditor | |
| height="100%" | |
| language={language} | |
| theme={theme} | |
| value={code} | |
| onChange={setCode} | |
| onMount={handleEditorMount} | |
| options={{ | |
| fontSize, | |
| minimap: { enabled: false }, | |
| wordWrap: 'on', | |
| scrollBeyondLastLine: false, | |
| automaticLayout: true | |
| /> | |
| </div> | |
| </div> | |
| <div className="bg-gray-100 rounded-lg p-4 overflow-auto"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <h3 className="font-bold text-gray-800">Output Console</h3> | |
| <button | |
| onClick={() => setOutput('')} | |
| className="text-sm text-gray-500 hover:text-gray-700" | |
| > | |
| Clear | |
| </button> | |
| </div> | |
| {error ? ( | |
| <div className="text-red-600 p-3 bg-red-50 rounded-lg"> | |
| <strong>Error:</strong> {error} | |
| </div> | |
| ) : ( | |
| <div className="bg-white p-3 rounded-lg h-[calc(100%-50px)] overflow-auto"> | |
| {output ? ( | |
| <SyntaxHighlighter language={language} style={atomDark} customStyle={{ background: 'transparent' }}> | |
| {output} | |
| </SyntaxHighlighter> | |
| ) : ( | |
| <p className="text-gray-500">Code execution output will appear here...</p> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {savedSnippets.length > 0 && ( | |
| <div className="mt-4 bg-gray-800 rounded-lg p-4"> | |
| <h3 className="font-bold text-white mb-3">Saved Snippets</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | |
| {savedSnippets.map((snippet) => ( | |
| <div key={snippet.id} className="bg-gray-700 rounded-lg p-3 hover:bg-gray-600 transition-colors cursor-pointer" onClick={() => handleLoadSnippet(snippet)}> | |
| <div className="flex justify-between items-start"> | |
| <div> | |
| <h4 className="font-medium text-white">{snippet.name}</h4> | |
| <p className="text-xs text-gray-400">{snippet.language}</p> | |
| </div> | |
| <span className="text-xs text-gray-400"> | |
| {new Date(snippet.createdAt).toLocaleDateString()} | |
| </span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } |