| import { useState, useRef, useEffect, useCallback } from 'react'; |
| import Editor from '@monaco-editor/react'; |
| import { motion, AnimatePresence } from 'motion/react'; |
| import { |
| Play, |
| RotateCcw, |
| Copy, |
| Download, |
| Terminal, |
| CheckCircle2, |
| XCircle, |
| Loader2, |
| Code2, |
| FileCode2, |
| Cpu, |
| Activity, |
| Braces, |
| ChevronDown, |
| ChevronUp, |
| Clock, |
| Maximize2, |
| Minimize2, |
| Trash2, |
| type LucideIcon, |
| } from 'lucide-react'; |
| import { cn } from '@/lib/utils'; |
| import { getCodeEditorFontSize } from '@/lib/preferences'; |
|
|
| |
|
|
| interface LanguageDef { |
| id: string; |
| label: string; |
| monacoId: string; |
| icon: string; |
| extension: string; |
| accent: string; |
| glow: string; |
| dot: string; |
| template: string; |
| } |
|
|
| const LANGUAGES: LanguageDef[] = [ |
| { |
| id: 'python', |
| label: 'Python', |
| monacoId: 'python', |
| icon: 'PY', |
| extension: 'py', |
| accent: 'from-emerald-300 to-teal-300', |
| glow: 'shadow-emerald-500/25', |
| dot: 'bg-emerald-300', |
| template: `# RYP Online Compiler - Python |
| # Write your code below and click Run |
| |
| name = input("Enter your name: ") |
| print(f"Hello, {name}! Welcome to RYP.") |
| |
| # Try more: |
| nums = [int(x) for x in input("Enter numbers (space-separated): ").split()] |
| print(f"Sum = {sum(nums)}") |
| print(f"Max = {max(nums)}") |
| print(f"Min = {min(nums)}") |
| `, |
| }, |
| { |
| id: 'cpp', |
| label: 'C++', |
| monacoId: 'cpp', |
| icon: 'C++', |
| extension: 'cpp', |
| accent: 'from-sky-300 to-blue-400', |
| glow: 'shadow-sky-500/25', |
| dot: 'bg-sky-300', |
| template: `// RYP Online Compiler - C++ |
| #include <iostream> |
| #include <vector> |
| #include <algorithm> |
| using namespace std; |
| |
| int main() { |
| string name; |
| cout << "Enter your name: "; |
| getline(cin, name); |
| cout << "Hello, " << name << "! Welcome to RYP." << endl; |
| |
| int n; |
| cout << "How many numbers? "; |
| cin >> n; |
| |
| vector<int> nums(n); |
| cout << "Enter " << n << " numbers: "; |
| for (int i = 0; i < n; i++) { |
| cin >> nums[i]; |
| } |
| |
| int total = 0; |
| for (int x : nums) total += x; |
| |
| cout << "Sum = " << total << endl; |
| cout << "Max = " << *max_element(nums.begin(), nums.end()) << endl; |
| cout << "Min = " << *min_element(nums.begin(), nums.end()) << endl; |
| |
| return 0; |
| } |
| `, |
| }, |
| { |
| id: 'java', |
| label: 'Java', |
| monacoId: 'java', |
| icon: 'JV', |
| extension: 'java', |
| accent: 'from-amber-300 to-orange-400', |
| glow: 'shadow-amber-500/25', |
| dot: 'bg-amber-300', |
| template: `// RYP Online Compiler - Java |
| import java.util.Scanner; |
| import java.util.Arrays; |
| |
| public class Main { |
| public static void main(String[] args) { |
| Scanner sc = new Scanner(System.in); |
| |
| System.out.print("Enter your name: "); |
| String name = sc.nextLine(); |
| System.out.println("Hello, " + name + "! Welcome to RYP."); |
| |
| System.out.print("How many numbers? "); |
| int n = sc.nextInt(); |
| int[] nums = new int[n]; |
| System.out.print("Enter " + n + " numbers: "); |
| for (int i = 0; i < n; i++) { |
| nums[i] = sc.nextInt(); |
| } |
| |
| int sum = Arrays.stream(nums).sum(); |
| int max = Arrays.stream(nums).max().orElse(0); |
| int min = Arrays.stream(nums).min().orElse(0); |
| |
| System.out.println("Sum = " + sum); |
| System.out.println("Max = " + max); |
| System.out.println("Min = " + min); |
| } |
| } |
| `, |
| }, |
| { |
| id: 'javascript', |
| label: 'JavaScript', |
| monacoId: 'javascript', |
| icon: 'JS', |
| extension: 'js', |
| accent: 'from-yellow-200 to-amber-300', |
| glow: 'shadow-yellow-500/20', |
| dot: 'bg-yellow-200', |
| template: `// RYP Online Compiler - JavaScript (Node.js) |
| // Note: Uses readline for stdin |
| |
| const readline = require('readline'); |
| const rl = readline.createInterface({ |
| input: process.stdin, |
| output: process.stdout, |
| }); |
| |
| function ask(question) { |
| return new Promise(resolve => rl.question(question, resolve)); |
| } |
| |
| (async () => { |
| const name = await ask("Enter your name: "); |
| console.log(\`Hello, \${name}! Welcome to RYP.\`); |
| |
| const input = await ask("Enter numbers (space-separated): "); |
| const nums = input.split(" ").map(Number); |
| |
| console.log(\`Sum = \${nums.reduce((a, b) => a + b, 0)}\`); |
| console.log(\`Max = \${Math.max(...nums)}\`); |
| console.log(\`Min = \${Math.min(...nums)}\`); |
| |
| rl.close(); |
| })(); |
| `, |
| }, |
| { |
| id: 'go', |
| label: 'Go', |
| monacoId: 'go', |
| icon: 'GO', |
| extension: 'go', |
| accent: 'from-cyan-200 to-sky-300', |
| glow: 'shadow-cyan-500/25', |
| dot: 'bg-cyan-200', |
| template: `// RYP Online Compiler - Go |
| package main |
| |
| import ( |
| \t"bufio" |
| \t"fmt" |
| \t"os" |
| ) |
| |
| func main() { |
| \treader := bufio.NewReader(os.Stdin) |
| |
| \tfmt.Print("Enter your name: ") |
| \tname, _ := reader.ReadString('\\n') |
| \tfmt.Printf("Hello, %s! Welcome to RYP.\\n", name[:len(name)-1]) |
| |
| \tvar n int |
| \tfmt.Print("How many numbers? ") |
| \tfmt.Scan(&n) |
| |
| \tnums := make([]int, n) |
| \tfmt.Printf("Enter %d numbers: ", n) |
| \tfor i := 0; i < n; i++ { |
| \t\tfmt.Scan(&nums[i]) |
| \t} |
| |
| \tsum, mx, mn := 0, nums[0], nums[0] |
| \tfor _, v := range nums { |
| \t\tsum += v |
| \t\tif v > mx { mx = v } |
| \t\tif v < mn { mn = v } |
| \t} |
| |
| \tfmt.Printf("Sum = %d\\nMax = %d\\nMin = %d\\n", sum, mx, mn) |
| } |
| `, |
| }, |
| { |
| id: 'typescript', |
| label: 'TypeScript', |
| monacoId: 'typescript', |
| icon: 'TS', |
| extension: 'ts', |
| accent: 'from-blue-300 to-indigo-300', |
| glow: 'shadow-blue-500/25', |
| dot: 'bg-blue-300', |
| template: `// RYP Online Compiler - TypeScript (Node.js) |
| // Note: Uses readline for stdin |
| |
| const readline = require('readline'); |
| const rl = readline.createInterface({ |
| input: process.stdin, |
| output: process.stdout, |
| }); |
| |
| function ask(question: string): Promise<string> { |
| return new Promise(resolve => rl.question(question, resolve)); |
| } |
| |
| (async () => { |
| const name = await ask("Enter your name: "); |
| console.log(\`Hello, \${name}! Welcome to RYP.\`); |
| |
| const input = await ask("Enter numbers (space-separated): "); |
| const nums = input.split(" ").map(Number); |
| |
| console.log(\`Sum = \${nums.reduce((a: number, b: number) => a + b, 0)}\`); |
| console.log(\`Max = \${Math.max(...nums)}\`); |
| console.log(\`Min = \${Math.min(...nums)}\`); |
| |
| rl.close(); |
| })(); |
| `, |
| }, |
| { |
| id: 'c', |
| label: 'C', |
| monacoId: 'c', |
| icon: 'C', |
| extension: 'c', |
| accent: 'from-rose-300 to-pink-300', |
| glow: 'shadow-rose-500/20', |
| dot: 'bg-rose-300', |
| template: `// RYP Online Compiler - C |
| #include <stdio.h> |
| |
| int main() { |
| char name[100]; |
| printf("Enter your name: "); |
| fgets(name, sizeof(name), stdin); |
| |
| // Remove newline |
| for (int i = 0; name[i]; i++) { |
| if (name[i] == '\\n') { name[i] = '\\0'; break; } |
| } |
| |
| printf("Hello, %s! Welcome to RYP.\\n", name); |
| |
| int n; |
| printf("How many numbers? "); |
| scanf("%d", &n); |
| |
| int nums[100], sum = 0, mx, mn; |
| printf("Enter %d numbers: ", n); |
| for (int i = 0; i < n; i++) { |
| scanf("%d", &nums[i]); |
| sum += nums[i]; |
| if (i == 0) { mx = mn = nums[0]; } |
| else { |
| if (nums[i] > mx) mx = nums[i]; |
| if (nums[i] < mn) mn = nums[i]; |
| } |
| } |
| |
| printf("Sum = %d\\nMax = %d\\nMin = %d\\n", sum, mx, mn); |
| return 0; |
| } |
| `, |
| }, |
| ]; |
|
|
| |
|
|
| interface ExecResult { |
| success: boolean; |
| stdout: string; |
| stderr: string; |
| output: string; |
| error: string; |
| exitCode: number; |
| executionTime: number; |
| durationMs: number; |
| } |
|
|
| async function executeCode( |
| language: string, |
| code: string, |
| input: string, |
| ): Promise<ExecResult> { |
| const res = await fetch('/api/execute', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ language, code, input }), |
| }); |
|
|
| if (!res.ok) { |
| const text = await res.text(); |
| let errMsg = 'Execution failed'; |
| try { |
| const j = JSON.parse(text); |
| errMsg = j.error || errMsg; |
| } catch { |
| errMsg = text || errMsg; |
| } |
| return { |
| success: false, |
| stdout: '', |
| stderr: errMsg, |
| output: '', |
| error: errMsg, |
| exitCode: -1, |
| executionTime: 0, |
| durationMs: 0, |
| }; |
| } |
|
|
| return res.json(); |
| } |
|
|
| |
|
|
| export default function CodingPage({ fontSize = 'medium' }: { fontSize?: 'small' | 'medium' | 'large' }) { |
| const [language, setLanguage] = useState<string>('python'); |
| const [code, setCode] = useState<string>(LANGUAGES[0].template); |
| const [userInput, setUserInput] = useState<string>(''); |
| const [isRunning, setIsRunning] = useState(false); |
| const [result, setResult] = useState<ExecResult | null>(null); |
| const [showInput, setShowInput] = useState(true); |
| const [isFullscreen, setIsFullscreen] = useState(false); |
| const [copied, setCopied] = useState(false); |
| const [filename, setFilename] = useState('main'); |
| const editorRef = useRef<any>(null); |
| const containerRef = useRef<HTMLDivElement>(null); |
|
|
| const currentLang = LANGUAGES.find((l) => l.id === language) ?? LANGUAGES[0]; |
| const codeLineCount = code.trim() ? code.split(/\r\n|\r|\n/).length : 0; |
| const inputLineCount = userInput.trim() ? userInput.split(/\r\n|\r|\n/).length : 0; |
| const errorText = result?.error || result?.stderr || ''; |
| const visibleOutput = result?.stdout || errorText || ''; |
| const outputLineCount = visibleOutput ? visibleOutput.split(/\r\n|\r|\n/).length : 0; |
| const executionDuration = result?.executionTime || result?.durationMs || 0; |
| const runStatus = isRunning |
| ? 'Executing' |
| : result |
| ? result.success |
| ? 'Last run passed' |
| : 'Review output' |
| : 'Ready'; |
|
|
| |
| const handleLanguageChange = useCallback( |
| (langId: string) => { |
| const lang = LANGUAGES.find((l) => l.id === langId); |
| if (!lang) return; |
| setLanguage(langId); |
| setCode(lang.template); |
| setResult(null); |
| }, |
| [], |
| ); |
|
|
| |
| const handleReset = useCallback(() => { |
| setCode(currentLang.template); |
| setResult(null); |
| setUserInput(''); |
| setFilename('main'); |
| }, [currentLang]); |
|
|
| |
| const handleRun = useCallback(async () => { |
| setIsRunning(true); |
| setResult(null); |
|
|
| try { |
| const res = await executeCode(language, code, userInput); |
| setResult(res); |
| } catch (err: any) { |
| setResult({ |
| success: false, |
| stdout: '', |
| stderr: err.message || 'Unknown error', |
| output: '', |
| error: err.message || 'Unknown error', |
| exitCode: -1, |
| executionTime: 0, |
| durationMs: 0, |
| }); |
| } finally { |
| setIsRunning(false); |
| } |
| }, [language, code, userInput]); |
|
|
| |
| useEffect(() => { |
| const handleKeyDown = (e: KeyboardEvent) => { |
| if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { |
| e.preventDefault(); |
| if (!isRunning) handleRun(); |
| } |
| }; |
| window.addEventListener('keydown', handleKeyDown); |
| return () => window.removeEventListener('keydown', handleKeyDown); |
| }, [handleRun, isRunning]); |
|
|
| |
| const handleCopy = useCallback(() => { |
| navigator.clipboard.writeText(code); |
| setCopied(true); |
| setTimeout(() => setCopied(false), 2000); |
| }, [code]); |
|
|
| |
| const handleDownload = useCallback(() => { |
| const blob = new Blob([code], { type: 'text/plain' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `${filename}.${currentLang.extension}`; |
| a.click(); |
| URL.revokeObjectURL(url); |
| }, [code, currentLang, filename]); |
|
|
| |
| const toggleFullscreen = useCallback(() => { |
| if (!containerRef.current) return; |
| if (!document.fullscreenElement) { |
| containerRef.current.requestFullscreen().catch(() => {}); |
| setIsFullscreen(true); |
| } else { |
| document.exitFullscreen().catch(() => {}); |
| setIsFullscreen(false); |
| } |
| }, []); |
|
|
| useEffect(() => { |
| const handler = () => { |
| setIsFullscreen(!!document.fullscreenElement); |
| |
| requestAnimationFrame(() => editorRef.current?.layout()); |
| const t = setTimeout(() => editorRef.current?.layout(), 300); |
| const t2 = setTimeout(() => editorRef.current?.layout(), 600); |
| return () => { |
| clearTimeout(t); |
| clearTimeout(t2); |
| }; |
| }; |
| document.addEventListener('fullscreenchange', handler); |
| return () => document.removeEventListener('fullscreenchange', handler); |
| }, []); |
|
|
| |
| useEffect(() => { |
| const layout = () => { |
| requestAnimationFrame(() => editorRef.current?.layout()); |
| }; |
| window.addEventListener('resize', layout); |
| return () => window.removeEventListener('resize', layout); |
| }, []); |
|
|
| useEffect(() => { |
| requestAnimationFrame(() => editorRef.current?.layout()); |
| const t = setTimeout(() => editorRef.current?.layout(), 250); |
| return () => clearTimeout(t); |
| }, [showInput]); |
|
|
| return ( |
| <div |
| ref={containerRef} |
| className={cn( |
| 'compiler-shell relative isolate flex flex-col overflow-hidden border border-white/10 bg-[#060911] text-white shadow-2xl shadow-black/40', |
| isFullscreen ? 'h-screen rounded-none' : 'h-[calc(100vh-5rem)] rounded-lg lg:h-full', |
| )} |
| > |
| {/* ββ Header Bar βββββββββββββββββββββββββββββββββββββββββββββββββββ */} |
| <header className="relative z-10 shrink-0 border-b border-white/10 bg-[#070b13]/85 backdrop-blur-2xl"> |
| <div className="flex flex-wrap items-center gap-3 px-3 py-3 sm:px-5 lg:px-6"> |
| <motion.div |
| initial={{ opacity: 0, x: -12 }} |
| animate={{ opacity: 1, x: 0 }} |
| transition={{ duration: 0.35, ease: 'easeOut' }} |
| className="flex min-w-[220px] flex-1 items-center gap-3" |
| > |
| <div |
| className={cn( |
| 'relative flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br text-[#061016] shadow-lg', |
| currentLang.accent, |
| currentLang.glow, |
| )} |
| > |
| <Cpu size={19} /> |
| <span className={cn('compiler-pulse absolute -right-1 -top-1 h-3 w-3 rounded-full ring-4 ring-[#070b13]', currentLang.dot)} /> |
| </div> |
| <div className="min-w-0"> |
| <div className="flex items-center gap-2"> |
| <h1 className="truncate text-base font-black tracking-wide text-white"> |
| Online Compiler |
| </h1> |
| <span |
| className={cn( |
| 'hidden rounded-md bg-gradient-to-r px-2 py-0.5 text-[10px] font-black text-[#061016] shadow-sm sm:inline-flex', |
| currentLang.accent, |
| )} |
| > |
| {currentLang.icon} |
| </span> |
| </div> |
| <div className="mt-1 flex items-center gap-2 text-[11px] font-semibold text-slate-500"> |
| <span className={cn('h-1.5 w-1.5 rounded-full', currentLang.dot)} /> |
| <span>{runStatus}</span> |
| </div> |
| </div> |
| </motion.div> |
| |
| <div className="relative order-3 w-full sm:order-none sm:w-auto"> |
| <Code2 |
| size={15} |
| className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" |
| /> |
| <select |
| id="language-select" |
| value={language} |
| onChange={(e) => handleLanguageChange(e.target.value)} |
| className="h-10 w-full appearance-none rounded-lg border border-white/10 bg-white/[0.06] pl-9 pr-10 text-xs font-black text-white outline-none transition-all hover:border-white/20 focus:border-cyan-300/50 focus:bg-white/[0.08] focus:ring-2 focus:ring-cyan-300/15 sm:w-[190px]" |
| > |
| {LANGUAGES.map((lang) => ( |
| <option |
| key={lang.id} |
| value={lang.id} |
| className="bg-[#0c1018]" |
| > |
| {lang.icon} / {lang.label} |
| </option> |
| ))} |
| </select> |
| <ChevronDown |
| size={14} |
| className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-slate-500" |
| /> |
| </div> |
| |
| <div className="ml-auto flex items-center gap-2"> |
| <ToolbarBtn |
| icon={Copy} |
| label={copied ? 'Copied' : 'Copy'} |
| onClick={handleCopy} |
| active={copied} |
| /> |
| <ToolbarBtn |
| icon={Download} |
| label="Download" |
| onClick={handleDownload} |
| /> |
| <ToolbarBtn |
| icon={RotateCcw} |
| label="Reset" |
| onClick={handleReset} |
| /> |
| <ToolbarBtn |
| icon={isFullscreen ? Minimize2 : Maximize2} |
| label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} |
| onClick={toggleFullscreen} |
| /> |
| <div className="hidden h-7 w-px bg-white/10 sm:block" /> |
| <button |
| id="run-button" |
| onClick={handleRun} |
| disabled={isRunning} |
| className={cn( |
| 'group flex h-10 items-center gap-2 rounded-lg bg-gradient-to-r px-4 text-xs font-black text-[#061016] shadow-lg transition-all hover:-translate-y-0.5 hover:brightness-110 active:translate-y-0 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-60 sm:px-5', |
| currentLang.accent, |
| currentLang.glow, |
| )} |
| > |
| {isRunning ? ( |
| <Loader2 size={16} className="animate-spin" /> |
| ) : ( |
| <Play size={16} className="transition-transform group-hover:translate-x-0.5" /> |
| )} |
| <span>{isRunning ? 'Running...' : 'Run'}</span> |
| </button> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-3 border-t border-white/5 bg-black/10 px-3 py-2 sm:px-5 lg:px-6"> |
| <CompilerMetric icon={FileCode2} label="Lines" value={codeLineCount.toString()} /> |
| <CompilerMetric icon={Terminal} label="Input" value={`${inputLineCount} ln`} /> |
| <CompilerMetric icon={Activity} label="Output" value={result ? `${outputLineCount} ln` : 'idle'} /> |
| </div> |
| </header> |
| |
| {/* ββ Main Content βββββββββββββββββββββββββββββββββββββββββββββββββ */} |
| <div className="relative z-10 flex min-h-0 flex-1 flex-col gap-3 p-3 lg:p-4 xl:flex-row"> |
| {/* ββ Editor Panel βββββββββββββββββββββββββββββββββββββββββββ */} |
| <div className="compiler-panel relative flex min-h-[360px] flex-1 flex-col overflow-hidden rounded-lg border border-white/10 bg-[#080d16]/95 shadow-2xl shadow-black/30"> |
| {/* Editor file tab */} |
| <div className="flex h-11 items-center gap-2 border-b border-white/10 bg-white/[0.035] px-3 sm:px-4"> |
| <span |
| className={cn( |
| 'flex h-7 min-w-9 items-center justify-center rounded-md bg-gradient-to-br px-2 text-[10px] font-black text-[#061016] shadow-sm', |
| currentLang.accent, |
| )} |
| > |
| {currentLang.icon} |
| </span> |
| <FileCode2 size={14} className="text-slate-500" /> |
| <input |
| value={filename} |
| onChange={(e) => setFilename(e.target.value)} |
| onBlur={(e) => { |
| if (!e.target.value.trim()) setFilename('main'); |
| }} |
| onKeyDown={(e) => { |
| if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); |
| }} |
| className="w-32 border-b border-transparent bg-transparent text-xs font-semibold text-slate-300 outline-none transition-colors focus:border-cyan-300/40 focus:text-white" |
| spellCheck={false} |
| /> |
| <span className="text-xs text-slate-600">.{currentLang.extension}</span> |
| <span className="ml-auto hidden items-center gap-1.5 rounded-md border border-white/8 bg-black/20 px-2 py-1 text-[10px] font-bold uppercase tracking-[0.14em] text-slate-500 sm:inline-flex"> |
| <span className={cn('h-1.5 w-1.5 rounded-full', currentLang.dot)} /> |
| {currentLang.label} |
| </span> |
| </div> |
| |
| {/* Monaco Editor */} |
| <div className="relative min-h-0 flex-1"> |
| <div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-8 bg-gradient-to-b from-[#080d16] to-transparent" /> |
| <Editor |
| height="100%" |
| language={currentLang.monacoId} |
| theme="ryp-lab-dark" |
| value={code} |
| beforeMount={(monaco) => { |
| monaco.editor.defineTheme('ryp-lab-dark', { |
| base: 'vs-dark', |
| inherit: true, |
| rules: [ |
| { token: 'comment', foreground: '64748b', fontStyle: 'italic' }, |
| { token: 'keyword', foreground: '67e8f9' }, |
| { token: 'string', foreground: '86efac' }, |
| { token: 'number', foreground: 'fbbf24' }, |
| { token: 'type', foreground: 'c4b5fd' }, |
| ], |
| colors: { |
| 'editor.background': '#080d16', |
| 'editor.foreground': '#dbeafe', |
| 'editor.lineHighlightBackground': '#ffffff08', |
| 'editorLineNumber.foreground': '#334155', |
| 'editorLineNumber.activeForeground': '#a7f3d0', |
| 'editorCursor.foreground': '#5eead4', |
| 'editor.selectionBackground': '#155e7555', |
| 'editor.inactiveSelectionBackground': '#1e293b80', |
| 'editorIndentGuide.background1': '#1e293b', |
| 'editorIndentGuide.activeBackground1': '#38bdf8', |
| }, |
| }); |
| }} |
| onChange={(value) => setCode(value || '')} |
| onMount={(editorInstance) => { |
| editorRef.current = editorInstance; |
| editorInstance.layout(); |
| }} |
| options={{ |
| fontSize: getCodeEditorFontSize(fontSize), |
| lineHeight: 21, |
| fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace", |
| fontLigatures: true, |
| minimap: { enabled: true, maxColumn: 80 }, |
| scrollBeyondLastLine: false, |
| automaticLayout: true, |
| padding: { top: 16, bottom: 16 }, |
| smoothScrolling: true, |
| cursorSmoothCaretAnimation: 'on', |
| cursorBlinking: 'smooth', |
| renderLineHighlight: 'all', |
| renderWhitespace: 'selection', |
| stickyScroll: { enabled: true }, |
| scrollbar: { |
| vertical: 'auto', |
| horizontal: 'auto', |
| verticalScrollbarSize: 14, |
| horizontalScrollbarSize: 14, |
| useShadows: false, |
| }, |
| lineNumbers: 'on', |
| lineNumbersMinChars: 3, |
| wordWrap: 'on', |
| wordWrapColumn: 80, |
| wrappingIndent: 'same', |
| folding: true, |
| foldingHighlight: true, |
| foldingStrategy: 'auto', |
| showFoldingControls: 'always', |
| bracketPairColorization: { enabled: true }, |
| guides: { |
| bracketPairs: true, |
| indentation: true, |
| highlightActiveIndentation: true, |
| }, |
| hover: { enabled: true }, |
| suggest: { |
| showKeywords: true, |
| showSnippets: true, |
| showFunctions: true, |
| showConstructors: true, |
| showFields: true, |
| showVariables: true, |
| showClasses: true, |
| showStructs: true, |
| showInterfaces: true, |
| showModules: true, |
| showProperties: true, |
| showEvents: true, |
| showOperators: true, |
| showUnits: true, |
| showValues: true, |
| showConstants: true, |
| showEnums: true, |
| showEnumMembers: true, |
| showColors: true, |
| showFiles: true, |
| showReferences: true, |
| showFolders: true, |
| showTypeParameters: true, |
| showUsers: true, |
| showIssues: true, |
| }, |
| quickSuggestions: { |
| other: true, |
| comments: false, |
| strings: true, |
| }, |
| acceptSuggestionOnCommitCharacter: true, |
| acceptSuggestionOnEnter: 'on', |
| tabCompletion: 'on', |
| parameterHints: { |
| enabled: true, |
| cycle: true, |
| }, |
| autoClosingBrackets: 'always', |
| autoClosingQuotes: 'always', |
| autoSurround: 'languageDefined', |
| formatOnPaste: true, |
| formatOnType: true, |
| autoIndent: 'full', |
| matchBrackets: 'always', |
| autoClosingDelete: 'auto', |
| trimAutoWhitespace: true, |
| renderControlCharacters: false, |
| contextmenu: true, |
| mouseWheelZoom: true, |
| multiCursorModifier: 'ctrlCmd', |
| selectionHighlight: true, |
| occurrencesHighlight: 'multiFile', |
| }} |
| /> |
| </div> |
| </div> |
| |
| {/* ββ Input / Output Panel βββββββββββββββββββββββββββββββββββ */} |
| <div className="compiler-panel flex w-full min-h-[320px] flex-col overflow-hidden rounded-lg border border-white/10 bg-[#080d16]/95 shadow-2xl shadow-black/30 xl:w-[430px] xl:min-w-[360px] xl:max-w-[520px]"> |
| {/* ββ Input Section ββββββββββββββββββββββββββββββββββββββββ */} |
| <div className="border-b border-white/10 bg-white/[0.02]"> |
| <button |
| onClick={() => setShowInput((v) => !v)} |
| className="flex w-full items-center justify-between px-4 py-3 text-left transition-colors hover:bg-white/[0.04] sm:px-5" |
| > |
| <div className="flex items-center gap-2"> |
| <Terminal size={14} className="text-cyan-300" /> |
| <span className="text-[11px] font-black uppercase tracking-[0.18em] text-slate-400"> |
| Input (stdin) |
| </span> |
| <span className="rounded-md border border-white/8 bg-black/20 px-1.5 py-0.5 text-[10px] font-bold text-slate-600"> |
| {inputLineCount} ln |
| </span> |
| </div> |
| {showInput ? ( |
| <ChevronUp size={14} className="text-slate-500" /> |
| ) : ( |
| <ChevronDown size={14} className="text-slate-500" /> |
| )} |
| </button> |
| <AnimatePresence initial={false}> |
| {showInput && ( |
| <motion.div |
| initial={{ height: 0, opacity: 0 }} |
| animate={{ height: 'auto', opacity: 1 }} |
| exit={{ height: 0, opacity: 0 }} |
| transition={{ duration: 0.24, ease: 'easeOut' }} |
| className="overflow-hidden" |
| > |
| <div className="px-4 pb-4 sm:px-5"> |
| <textarea |
| id="stdin-input" |
| value={userInput} |
| onChange={(e) => setUserInput(e.target.value)} |
| placeholder="stdin" |
| spellCheck={false} |
| className="h-28 w-full resize-none rounded-lg border border-white/10 bg-black/35 px-4 py-3 font-mono text-sm leading-relaxed text-slate-300 outline-none transition-all placeholder:text-slate-700 focus:border-cyan-300/40 focus:ring-2 focus:ring-cyan-300/10" |
| /> |
| </div> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| </div> |
| |
| {/* ββ Output Section βββββββββββββββββββββββββββββββββββββββ */} |
| <div className="flex min-h-0 flex-1 flex-col"> |
| <div className="flex items-center justify-between border-b border-white/10 bg-white/[0.02] px-4 py-3 sm:px-5"> |
| <div className="flex items-center gap-2"> |
| <Terminal size={14} className="text-emerald-400" /> |
| <span className="text-[11px] font-black uppercase tracking-[0.18em] text-slate-400"> |
| Output |
| </span> |
| </div> |
| {result && ( |
| <div className="flex items-center gap-2 sm:gap-3"> |
| {executionDuration > 0 && ( |
| <span className="flex items-center gap-1 text-[10px] font-bold text-slate-500"> |
| <Clock size={11} /> |
| {executionDuration}ms |
| </span> |
| )} |
| {result.success ? ( |
| <span className="flex items-center gap-1 rounded-md border border-emerald-300/15 bg-emerald-400/10 px-2 py-0.5 text-[10px] font-black uppercase tracking-wider text-emerald-300"> |
| <CheckCircle2 size={11} /> |
| Success |
| </span> |
| ) : ( |
| <span className="flex items-center gap-1 rounded-md border border-rose-300/15 bg-rose-400/10 px-2 py-0.5 text-[10px] font-black uppercase tracking-wider text-rose-300"> |
| <XCircle size={11} /> |
| Error |
| </span> |
| )} |
| <button |
| onClick={() => setResult(null)} |
| className="rounded-md p-1 text-slate-600 transition-colors hover:bg-white/5 hover:text-slate-300" |
| title="Clear output" |
| > |
| <Trash2 size={12} /> |
| </button> |
| </div> |
| )} |
| </div> |
| |
| <div className="custom-scrollbar flex-1 overflow-auto p-4 sm:p-5"> |
| {isRunning ? ( |
| <motion.div |
| key="running" |
| initial={{ opacity: 0, y: 8 }} |
| animate={{ opacity: 1, y: 0 }} |
| className="flex flex-col items-center justify-center py-12 text-slate-500" |
| > |
| <div className="compiler-equalizer mb-4" aria-hidden="true"> |
| <span /> |
| <span /> |
| <span /> |
| <span /> |
| </div> |
| <p className="text-sm font-bold tracking-wide"> |
| Compiling and executing |
| </p> |
| <p className="mt-1 text-xs text-slate-600"> |
| {currentLang.label} |
| </p> |
| </motion.div> |
| ) : result ? ( |
| <motion.div |
| key="result" |
| initial={{ opacity: 0, y: 8 }} |
| animate={{ opacity: 1, y: 0 }} |
| className="space-y-3" |
| > |
| {/* Stdout */} |
| {result.stdout && ( |
| <pre |
| className={cn( |
| 'whitespace-pre-wrap break-words rounded-lg border p-4 font-mono text-sm leading-relaxed shadow-inner', |
| result.success |
| ? 'border-emerald-300/15 bg-emerald-400/[0.055] text-emerald-200' |
| : 'border-white/10 bg-white/[0.025] text-slate-300', |
| )} |
| > |
| {result.stdout} |
| </pre> |
| )} |
| |
| {/* Error / Stderr */} |
| {errorText && ( |
| <pre className="whitespace-pre-wrap break-words rounded-lg border border-rose-300/15 bg-rose-400/[0.055] p-4 font-mono text-sm leading-relaxed text-rose-200 shadow-inner"> |
| {errorText} |
| </pre> |
| )} |
| |
| {/* If neither stdout nor error */} |
| {!result.stdout && !errorText && ( |
| <div className="py-8 text-center text-sm text-slate-600"> |
| Program executed with no output. |
| </div> |
| )} |
| </motion.div> |
| ) : ( |
| /* Empty state */ |
| <motion.div |
| key="empty" |
| initial={{ opacity: 0, y: 8 }} |
| animate={{ opacity: 1, y: 0 }} |
| className="flex flex-col items-center justify-center py-12 text-center text-slate-600" |
| > |
| <div className="mb-4 flex h-14 w-14 items-center justify-center rounded-lg border border-white/8 bg-white/[0.03] text-slate-700"> |
| <Braces size={26} /> |
| </div> |
| <p className="text-sm font-bold tracking-wide"> |
| Awaiting execution |
| </p> |
| <p className="mt-1 max-w-[240px] text-xs text-slate-700"> |
| stdout and stderr |
| </p> |
| </motion.div> |
| )} |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
|
|
| function ToolbarBtn({ |
| icon: Icon, |
| label, |
| onClick, |
| active, |
| }: { |
| icon: LucideIcon; |
| label: string; |
| onClick: () => void; |
| active?: boolean; |
| }) { |
| return ( |
| <button |
| onClick={onClick} |
| title={label} |
| aria-label={label} |
| className={cn( |
| 'flex h-9 w-9 items-center justify-center rounded-lg border text-slate-500 transition-all hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.96]', |
| active |
| ? 'border-emerald-300/30 bg-emerald-400/10 text-emerald-300 shadow-lg shadow-emerald-500/10' |
| : 'border-white/10 bg-white/[0.035] hover:border-white/20 hover:bg-white/[0.07] hover:text-white', |
| )} |
| > |
| <Icon size={15} /> |
| </button> |
| ); |
| } |
|
|
| function CompilerMetric({ |
| icon: Icon, |
| label, |
| value, |
| }: { |
| icon: LucideIcon; |
| label: string; |
| value: string; |
| }) { |
| return ( |
| <div className="flex min-w-0 items-center justify-center gap-2 border-r border-white/5 px-2 text-[10px] font-bold uppercase tracking-[0.14em] text-slate-500 last:border-r-0 sm:justify-start"> |
| <Icon size={12} className="shrink-0 text-slate-600" /> |
| <span className="hidden sm:inline">{label}</span> |
| <span className="truncate text-slate-300">{value}</span> |
| </div> |
| ); |
| } |
|
|