RYP / src /components /Compiler /CodeEditorPanel.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
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();
// Free tier: lock C++ (value: 'cpp')
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;
// Define RYP dark theme
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');
// Track cursor position
editor.onDidChangeCursorPosition((e: any) => {
onCursorChange(e.position.lineNumber, e.position.column);
});
// Ctrl+Enter = run
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
onCtrlEnter();
});
// Tab inserts 4 spaces
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>
);
}