| import { useCallback, useRef } from 'react'; |
| import Editor, { type OnMount } from '@monaco-editor/react'; |
| import { Plus, RotateCcw, X, Download, Lock } from 'lucide-react'; |
| import { LANGUAGE_OPTIONS, DEFAULT_CODE_BY_LANGUAGE } from './useCompiler.js'; |
| import { useSubscription } from '@/contexts/SubscriptionContext'; |
|
|
| function getLang(value: string) { |
| return LANGUAGE_OPTIONS.find((l: any) => l.value === value) ?? LANGUAGE_OPTIONS[0]; |
| } |
|
|
| export interface EditorFile { |
| id: string; |
| name: string; |
| language: string; |
| code: string; |
| } |
|
|
| interface CodeEditorPanelProps { |
| files: EditorFile[]; |
| activeFileId: string; |
| onSelectFile: (id: string) => void; |
| onCloseFile: (id: string) => void; |
| onAddFile: () => void; |
| onCodeChange: (code: string) => void; |
| onLanguageChange: (lang: string) => void; |
| onResetCode: () => void; |
| onCursorChange: (line: number, col: number) => void; |
| onCtrlEnter: () => void; |
| } |
|
|
| export default function CodeEditorPanel({ |
| files, |
| activeFileId, |
| onSelectFile, |
| onCloseFile, |
| onAddFile, |
| onCodeChange, |
| onLanguageChange, |
| onResetCode, |
| onCursorChange, |
| onCtrlEnter, |
| }: CodeEditorPanelProps) { |
| const { tier } = useSubscription(); |
| |
| const LOCKED_LANGUAGES = tier === 'free' ? new Set(['cpp']) : new Set<string>(); |
| const activeFile = files.find((f) => f.id === activeFileId) ?? files[0]; |
| const activeLang = getLang(activeFile.language); |
| const editorRef = useRef<any>(null); |
|
|
| const handleDownload = useCallback(() => { |
| const blob = new Blob([activeFile.code], { type: 'text/plain' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = activeFile.name; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| URL.revokeObjectURL(url); |
| }, [activeFile]); |
|
|
| const handleMount: OnMount = useCallback( |
| (editor, monaco) => { |
| editorRef.current = editor; |
|
|
| |
| monaco.editor.defineTheme('ryp-dark', { |
| base: 'vs-dark', |
| inherit: true, |
| rules: [ |
| { token: '', foreground: 'e2e2f0', background: '000000' }, |
| { token: 'keyword', foreground: 'c084fc' }, |
| { token: 'string', foreground: '86efac' }, |
| { token: 'number', foreground: 'fbbf24' }, |
| { token: 'comment', foreground: '55556a', fontStyle: 'italic' }, |
| { token: 'type', foreground: '67e8f9' }, |
| { token: 'function', foreground: '60a5fa' }, |
| { token: 'variable', foreground: 'e2e2f0' }, |
| { token: 'operator', foreground: 'a78bfa' }, |
| ], |
| colors: { |
| 'editor.background': '#000000', |
| 'editor.foreground': '#e2e2f0', |
| 'editorLineNumber.foreground': '#333344', |
| 'editorLineNumber.activeForeground': '#8b5cf6', |
| 'editorCursor.foreground': '#8b5cf6', |
| 'editor.selectionBackground': '#8b5cf633', |
| 'editor.inactiveSelectionBackground': '#8b5cf611', |
| 'editor.lineHighlightBackground': '#111111', |
| 'editorGutter.background': '#000000', |
| 'editorWidget.background': '#111111', |
| 'editorWidget.border': '#222233', |
| 'editorBracketMatch.background': '#8b5cf633', |
| 'editorBracketMatch.border': '#8b5cf6', |
| }, |
| }); |
|
|
| monaco.editor.setTheme('ryp-dark'); |
|
|
| |
| editor.onDidChangeCursorPosition((e: any) => { |
| onCursorChange(e.position.lineNumber, e.position.column); |
| }); |
|
|
| |
| editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { |
| onCtrlEnter(); |
| }); |
|
|
| |
| editor.getModel()?.updateOptions({ tabSize: 4, insertSpaces: true }); |
| }, |
| [onCursorChange, onCtrlEnter], |
| ); |
|
|
| return ( |
| <div className="flex flex-col h-full" style={{ background: '#000000' }}> |
| {/* Tabs bar */} |
| <div className="ryp-editor-tabs"> |
| <div className="flex items-center gap-1 flex-1 min-w-0 overflow-x-auto"> |
| {files.map((file) => ( |
| <button |
| key={file.id} |
| onClick={() => onSelectFile(file.id)} |
| className={`ryp-editor-tab ${file.id === activeFileId ? 'ryp-editor-tab--active' : ''}`} |
| > |
| <span className="ryp-editor-tab__dot" /> |
| <span className="truncate max-w-[120px]">{file.name}</span> |
| {files.length > 1 && ( |
| <span |
| role="button" |
| tabIndex={0} |
| onClick={(e) => { |
| e.stopPropagation(); |
| onCloseFile(file.id); |
| }} |
| onKeyDown={(e) => { |
| if (e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| e.stopPropagation(); |
| onCloseFile(file.id); |
| } |
| }} |
| className="rounded p-0.5 hover:bg-white/10 transition-colors" |
| style={{ color: '#55556a', cursor: 'pointer' }} |
| > |
| <X size={12} /> |
| </span> |
| )} |
| </button> |
| ))} |
| <button |
| onClick={onAddFile} |
| className="ryp-editor-tab" |
| style={{ padding: '4px 8px' }} |
| title="New file" |
| > |
| <Plus size={14} /> |
| </button> |
| </div> |
| |
| <div className="flex items-center gap-2 ml-2 flex-shrink-0"> |
| <select |
| value={activeFile.language} |
| onChange={(e) => onLanguageChange(e.target.value)} |
| className="h-8 rounded-md px-2 text-xs font-bold outline-none transition" |
| style={{ |
| background: '#111111', |
| border: '1px solid #222233', |
| color: '#e2e2f0', |
| fontFamily: "'Inter', sans-serif", |
| }} |
| > |
| {LANGUAGE_OPTIONS.map((l: any) => { |
| const isLangLocked = LOCKED_LANGUAGES.has(l.value); |
| return ( |
| <option key={l.value} value={l.value} disabled={isLangLocked} |
| style={isLangLocked ? { color: '#55556a' } : {}}> |
| {l.label}{isLangLocked ? ' 🔒' : ''} |
| </option> |
| ); |
| })} |
| </select> |
| <button |
| onClick={handleDownload} |
| title="Download code" |
| className="flex items-center justify-center w-8 h-8 rounded-md transition-colors" |
| style={{ background: '#111111', border: '1px solid #222233', color: '#9090b0', cursor: 'pointer' }} |
| onMouseEnter={(e) => (e.currentTarget.style.color = '#e2e2f0')} |
| onMouseLeave={(e) => (e.currentTarget.style.color = '#9090b0')} |
| > |
| <Download size={14} /> |
| </button> |
| <button |
| onClick={onResetCode} |
| title="Reset code" |
| className="flex items-center justify-center w-8 h-8 rounded-md transition-colors" |
| style={{ background: '#111111', border: '1px solid #222233', color: '#9090b0', cursor: 'pointer' }} |
| onMouseEnter={(e) => (e.currentTarget.style.color = '#e2e2f0')} |
| onMouseLeave={(e) => (e.currentTarget.style.color = '#9090b0')} |
| > |
| <RotateCcw size={14} /> |
| </button> |
| </div> |
| </div> |
| |
| {/* Monaco editor */} |
| <div className="flex-1 min-h-0"> |
| <Editor |
| height="100%" |
| language={activeLang.monacoLanguage} |
| value={activeFile.code} |
| theme="ryp-dark" |
| onMount={handleMount} |
| onChange={(v) => onCodeChange(v ?? '')} |
| options={{ |
| minimap: { enabled: false }, |
| fontSize: 14, |
| fontLigatures: true, |
| fontFamily: "'JetBrains Mono', Consolas, monospace", |
| smoothScrolling: true, |
| cursorBlinking: 'smooth', |
| cursorSmoothCaretAnimation: 'on', |
| scrollBeyondLastLine: false, |
| automaticLayout: true, |
| padding: { top: 16, bottom: 16 }, |
| tabSize: 4, |
| insertSpaces: true, |
| renderWhitespace: 'none', |
| bracketPairColorization: { enabled: true }, |
| lineNumbers: 'on', |
| glyphMargin: false, |
| folding: true, |
| lineDecorationsWidth: 12, |
| overviewRulerBorder: false, |
| scrollbar: { |
| verticalScrollbarSize: 6, |
| horizontalScrollbarSize: 6, |
| }, |
| }} |
| /> |
| </div> |
| </div> |
| ); |
| } |
|
|