Spaces:
Build error
Build error
| import { useState, useEffect, useRef } from 'react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| import Editor from '@monaco-editor/react' | |
| import { | |
| FaPlay, FaSave, FaDownload, FaUpload, FaCopy, FaTerminal, | |
| FaCode, FaBug, FaCheckCircle, FaExclamationTriangle, FaCog, | |
| FaFolderOpen, FaFile, FaFileCode, FaJs, FaPython, FaJava, | |
| FaHtml5, FaCss3Alt, FaReact, FaNodeJs, FaGitAlt, FaDatabase | |
| } from 'react-icons/fa' | |
| import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' | |
| import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism' | |
| import toast from 'react-hot-toast' | |
| export default function CodeEditor({ language, theme }) { | |
| const [code, setCode] = useState(`// ${language === 'fa' ? 'کد خود را اینجا بنویسید' : 'Write your code here'}\nfunction hello() {\n console.log("Hello, World!");\n}`) | |
| const [output, setOutput] = useState('') | |
| const [isRunning, setIsRunning] = useState(false) | |
| const [selectedLanguage, setSelectedLanguage] = useState('javascript') | |
| const [files, setFiles] = useState([ | |
| { id: 1, name: 'main.js', content: code, language: 'javascript' }, | |
| { id: 2, name: 'utils.py', content: '# Python utilities', language: 'python' } | |
| ]) | |
| const [activeFile, setActiveFile] = useState(1) | |
| const [fontSize, setFontSize] = useState(14) | |
| const [theme, setEditorTheme] = useState('vs-dark') | |
| const [showTerminal, setShowTerminal] = useState(true) | |
| const [errors, setErrors] = useState([]) | |
| const editorRef = useRef(null) | |
| const languages = [ | |
| { id: 'javascript', name: 'JavaScript', icon: FaJs, color: 'text-yellow-400' }, | |
| { id: 'python', name: 'Python', icon: FaPython, color: 'text-blue-400' }, | |
| { id: 'java', name: 'Java', icon: FaJava, color: 'text-red-400' }, | |
| { id: 'html', name: 'HTML', icon: FaHtml5, color: 'text-orange-500' }, | |
| { id: 'css', name: 'CSS', icon: FaCss3Alt, color: 'text-blue-500' }, | |
| { id: 'typescript', name: 'TypeScript', icon: FaCode, color: 'text-blue-600' }, | |
| { id: 'json', name: 'JSON', icon: FaDatabase, color: 'text-green-500' }, | |
| { id: 'sql', name: 'SQL', icon: FaDatabase, color: 'text-purple-500' } | |
| ] | |
| const handleEditorDidMount = (editor, monaco) => { | |
| editorRef.current = editor | |
| editor.focus() | |
| } | |
| const runCode = async () => { | |
| setIsRunning(true) | |
| setOutput('') | |
| setErrors([]) | |
| try { | |
| // Simulate code execution | |
| await new Promise(resolve => setTimeout(resolve, 2000)) | |
| if (selectedLanguage === 'javascript') { | |
| // Safe eval simulation | |
| const result = 'Code executed successfully!\nOutput: Hello, World!' | |
| setOutput(result) | |
| toast.success(language === 'fa' ? 'کد با موفقیت اجرا شد' : 'Code executed successfully') | |
| } else { | |
| setOutput(`${selectedLanguage.toUpperCase()} code execution simulated`) | |
| } | |
| } catch (error) { | |
| setErrors([error.message]) | |
| toast.error(language === 'fa' ? 'خطا در اجرای کد' : 'Error executing code') | |
| } finally { | |
| setIsRunning(false) | |
| } | |
| } | |
| const saveCode = () => { | |
| const updatedFiles = files.map(file => | |
| file.id === activeFile ? { ...file, content: code } : file | |
| ) | |
| setFiles(updatedFiles) | |
| toast.success(language === 'fa' ? 'کد ذخیره شد' : 'Code saved') | |
| } | |
| const downloadCode = () => { | |
| const file = files.find(f => f.id === activeFile) | |
| const blob = new Blob([code], { type: 'text/plain' }) | |
| const url = URL.createObjectURL(blob) | |
| const a = document.createElement('a') | |
| a.href = url | |
| a.download = file.name | |
| a.click() | |
| toast.success(language === 'fa' ? 'کد دانلود شد' : 'Code downloaded') | |
| } | |
| const uploadFile = (event) => { | |
| const file = event.target.files[0] | |
| if (file) { | |
| const reader = new FileReader() | |
| reader.onload = (e) => { | |
| const content = e.target.result | |
| const newFile = { | |
| id: Date.now(), | |
| name: file.name, | |
| content: content, | |
| language: selectedLanguage | |
| } | |
| setFiles([...files, newFile]) | |
| setActiveFile(newFile.id) | |
| setCode(content) | |
| toast.success(language === 'fa' ? 'فایل آپلود شد' : 'File uploaded') | |
| } | |
| reader.readAsText(file) | |
| } | |
| } | |
| const createNewFile = () => { | |
| const newFile = { | |
| id: Date.now(), | |
| name: `untitled.${selectedLanguage === 'javascript' ? 'js' : selectedLanguage}`, | |
| content: `// New ${selectedLanguage} file`, | |
| language: selectedLanguage | |
| } | |
| setFiles([...files, newFile]) | |
| setActiveFile(newFile.id) | |
| setCode(newFile.content) | |
| } | |
| const deleteFile = (fileId) => { | |
| if (files.length > 1) { | |
| setFiles(files.filter(f => f.id !== fileId)) | |
| if (activeFile === fileId) { | |
| setActiveFile(files[0].id) | |
| setCode(files[0].content) | |
| } | |
| toast.success(language === 'fa' ? 'فایل حذف شد' : 'File deleted') | |
| } | |
| } | |
| useEffect(() => { | |
| const activeFileData = files.find(f => f.id === activeFile) | |
| if (activeFileData) { | |
| setCode(activeFileData.content) | |
| setSelectedLanguage(activeFileData.language) | |
| } | |
| }, [activeFile]) | |
| return ( | |
| <div className="flex flex-col h-full space-y-4"> | |
| {/* Toolbar */} | |
| <div className="flex items-center justify-between p-4 bg-gray-800 rounded-lg"> | |
| <div className="flex items-center space-x-reverse space-x-3"> | |
| <button | |
| onClick={runCode} | |
| disabled={isRunning} | |
| className="flex items-center space-x-reverse space-x-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 transition-all" | |
| > | |
| <FaPlay className="w-4 h-4" /> | |
| <span>{isRunning ? (language === 'fa' ? 'در حال اجرا...' : 'Running...') : (language === 'fa' ? 'اجرا' : 'Run')}</span> | |
| </button> | |
| <button | |
| onClick={saveCode} | |
| className="flex items-center space-x-reverse space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all" | |
| > | |
| <FaSave className="w-4 h-4" /> | |
| <span>{language === 'fa' ? 'ذخیره' : 'Save'}</span> | |
| </button> | |
| <button | |
| onClick={downloadCode} | |
| className="flex items-center space-x-reverse space-x-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-all" | |
| > | |
| <FaDownload className="w-4 h-4" /> | |
| <span>{language === 'fa' ? 'دانلود' : 'Download'}</span> | |
| </button> | |
| <label className="flex items-center space-x-reverse space-x-2 px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 cursor-pointer transition-all"> | |
| <FaUpload className="w-4 h-4" /> | |
| <span>{language === 'fa' ? 'آپلود' : 'Upload'}</span> | |
| <input type="file" onChange={uploadFile} className="hidden" accept=".js,.py,.java,.html,.css,.json,.sql,.ts,.tsx" /> | |
| </label> | |
| <button | |
| onClick={createNewFile} | |
| className="flex items-center space-x-reverse space-x-2 px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-all" | |
| > | |
| <FaFileCode className="w-4 h-4" /> | |
| <span>{language === 'fa' ? 'فایل جدید' : 'New File'}</span> | |
| </button> | |
| </div> | |
| <div className="flex items-center space-x-reverse space-x-3"> | |
| <select | |
| value={selectedLanguage} | |
| onChange={(e) => setSelectedLanguage(e.target.value)} | |
| className="px-3 py-2 bg-gray-700 text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" | |
| > | |
| {languages.map(lang => ( | |
| <option key={lang.id} value={lang.id}>{lang.name}</option> | |
| ))} | |
| </select> | |
| <button | |
| onClick={() => setShowTerminal(!showTerminal)} | |
| className={`p-2 rounded-lg transition-all ${ | |
| showTerminal ? 'bg-primary-600 text-white' : 'bg-gray-700 text-gray-300' | |
| }`} | |
| > | |
| <FaTerminal className="w-4 h-4" /> | |
| </button> | |
| <button | |
| onClick={() => { | |
| setFontSize(prev => prev === 14 ? 16 : prev === 16 ? 18 : 14) | |
| toast.success(language === 'fa' ? `اندازه فونت: ${fontSize === 14 ? 16 : fontSize === 16 ? 18 : 14}px` : `Font size: ${fontSize === 14 ? 16 : fontSize === 16 ? 18 : 14}px`) | |
| className="p-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-all" | |
| > | |
| <FaCog className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* File Tabs */} | |
| <div className="flex items-center space-x-reverse space-x-1 p-2 bg-gray-800 rounded-lg overflow-x-auto"> | |
| {files.map(file => ( | |
| <button | |
| key={file.id} | |
| onClick={() => setActiveFile(file.id)} | |
| className={`flex items-center space-x-reverse space-x-2 px-3 py-2 rounded-lg transition-all whitespace-nowrap ${ | |
| activeFile === file.id | |
| ? 'bg-primary-600 text-white' | |
| : 'bg-gray-700 text-gray-300 hover:bg-gray-600' | |
| }`} | |
| > | |
| <FaFile className="w-3 h-3" /> | |
| <span className="text-sm">{file.name}</span> | |
| {files.length > 1 && ( | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation() | |
| deleteFile(file.id) | |
| className="opacity-50 hover:opacity-100 transition-opacity" | |
| > | |
| × | |
| </button> | |
| )} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Editor and Output */} | |
| <div className="flex-1 flex space-x-reverse space-x-4 min-h-0"> | |
| {/* Code Editor */} | |
| <div className="flex-1 bg-gray-800 rounded-lg overflow-hidden"> | |
| <Editor | |
| height="100%" | |
| language={selectedLanguage} | |
| value={code} | |
| onChange={(value) => setCode(value)} | |
| onMount={handleEditorDidMount} | |
| theme={theme} | |
| options={{ | |
| fontSize, | |
| minimap: { enabled: false }, | |
| scrollBeyondLastLine: false, | |
| automaticLayout: true, | |
| tabSize: 2, | |
| wordWrap: 'on', | |
| lineNumbers: 'on', | |
| folding: true, | |
| renderWhitespace: 'selection', | |
| /> | |
| </div> | |
| {/* Terminal/Output */} | |
| <AnimatePresence> | |
| {showTerminal && ( | |
| <motion.div | |
| initial={{ width: 0, opacity: 0 }} | |
| animate={{ width: '400px', opacity: 1 }} | |
| exit={{ width: 0, opacity: 0 }} | |
| className="bg-gray-800 rounded-lg overflow-hidden flex flex-col" | |
| > | |
| <div className="flex items-center justify-between p-3 bg-gray-900 border-b border-gray-700"> | |
| <div className="flex items-center space-x-reverse space-x-2"> | |
| <FaTerminal className="w-4 h-4 text-green-400" /> | |
| <span className="text-sm font-medium text-white"> | |
| {language === 'fa' ? 'خروجی' : 'Output'} | |
| </span> | |
| </div> | |
| <div className="flex items-center space-x-reverse space-x-2"> | |
| {isRunning && ( | |
| <div className="flex space-x-reverse space-x-1"> | |
| <div className="w-2 h-2 bg-green-400 rounded-full animate-bounce" /> | |
| <div className="w-2 h-2 bg-green-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} /> | |
| <div className="w-2 h-2 bg-green-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} /> | |
| </div> | |
| )} | |
| {errors.length === 0 && output && ( | |
| <FaCheckCircle className="w-4 h-4 text-green-400" /> | |
| )} | |
| {errors.length > 0 && ( | |
| <FaExclamationTriangle className="w-4 h-4 text-red-400" /> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex-1 p-4 overflow-y-auto font-mono text-sm"> | |
| {errors.length > 0 ? ( | |
| <div className="space-y-2"> | |
| {errors.map((error, index) => ( | |
| <div key={index} className="p-2 bg-red-900 bg-opacity-30 border border-red-600 rounded text-red-400"> | |
| <span className="font-medium">Error:</span> {error} | |
| </div> | |
| ))} | |
| </div> | |
| ) : output ? ( | |
| <pre className="text-green-400 whitespace-pre-wrap">{output}</pre> | |
| ) : ( | |
| <div className="text-gray-500"> | |
| {language === 'fa' ? 'کد را اجرا کنید تا خروجی نمایش داده شود' : 'Run the code to see output'} | |
| </div> | |
| )} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| </div> | |
| ) | |
| } |