RYP / src /components /CodingPage.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
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';
/* ────────────────────────────── Language Config ────────────────────────────── */
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;
}
`,
},
];
/* ────────────────────────────── Execution API ─────────────────────────────── */
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();
}
/* ────────────────────────────── Component ─────────────────────────────────── */
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';
// Handle language change
const handleLanguageChange = useCallback(
(langId: string) => {
const lang = LANGUAGES.find((l) => l.id === langId);
if (!lang) return;
setLanguage(langId);
setCode(lang.template);
setResult(null);
},
[],
);
// Reset code to template
const handleReset = useCallback(() => {
setCode(currentLang.template);
setResult(null);
setUserInput('');
setFilename('main');
}, [currentLang]);
// Run code
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]);
// Keyboard shortcut: Ctrl+Enter to run
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]);
// Copy code
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}, [code]);
// Download 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]);
// Toggle fullscreen
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);
// Layout AFTER the fullscreen transition finishes so dimensions are final
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);
}, []);
// Layout the editor on mount, input toggle, and window resize
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>
);
}
/* ────────────────────────────── Toolbar Button ────────────────────────────── */
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>
);
}