EduLab / frontend /components /CodeSandboxModal.tsx
rinogeek's picture
first commit
fafd0bb
import React, { useState, useEffect } from 'react';
import { X, Play, RefreshCw, Terminal, Code2, Trash2 } from 'lucide-react';
interface Props {
isOpen: boolean;
onClose: () => void;
}
type Language = 'javascript' | 'python';
const CodeSandboxModal: React.FC<Props> = ({ isOpen, onClose }) => {
const [language, setLanguage] = useState<Language>('javascript');
const [code, setCode] = useState('');
const [output, setOutput] = useState<string[]>([]);
const [isRunning, setIsRunning] = useState(false);
const DEFAULT_CODE = {
javascript: `// Bienvenue dans le Bac à Sable JS !
const nom = "EduLab";
const annee = 2025;
console.log("Bonjour " + nom);
console.log("Nous sommes en " + annee);
// Boucle simple
for(let i = 1; i <= 3; i++) {
console.log("Compteur : " + i);
}`,
python: `# Bienvenue dans le Bac à Sable Python !
nom = "EduLab"
score = 100
print("Bonjour " + nom)
print("Votre score est : " + str(score))
# Calcul simple
a = 5
b = 10
print("La somme de 5 + 10 est : " + str(a + b))`
};
useEffect(() => {
if (isOpen) {
setCode(DEFAULT_CODE[language]);
setOutput(['Prêt à exécuter le code...']);
}
}, [isOpen, language]);
if (!isOpen) return null;
const runJavaScript = () => {
const logs: string[] = [];
// Surcharge de console.log pour capturer la sortie
const originalLog = console.log;
console.log = (...args) => {
logs.push(args.map(arg => String(arg)).join(' '));
// originalLog(...args); // Décommenter pour voir aussi dans la console du navigateur
};
try {
// Exécution sécurisée via Function constructor
// eslint-disable-next-line no-new-func
new Function(code)();
} catch (error: any) {
logs.push(`Erreur : ${error.message}`);
} finally {
// Restauration de console.log
console.log = originalLog;
setOutput(logs.length > 0 ? logs : ['Code exécuté avec succès (aucune sortie).']);
}
};
const runPythonMock = () => {
// Simulation simple d'un interpréteur Python pour la démo
// Note: Un vrai interpréteur nécessiterait Pyodide (WASM) qui est trop lourd pour cette démo.
const logs: string[] = [];
const variables: Record<string, any> = {};
try {
const lines = code.split('\n');
lines.forEach(line => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) return;
// Gestion simple des print()
if (trimmed.startsWith('print(') && trimmed.endsWith(')')) {
let content = trimmed.slice(6, -1);
// Gestion basique de la concaténation de chaînes (+)
const parts = content.split('+').map(p => p.trim());
let result = '';
parts.forEach(part => {
if ((part.startsWith('"') && part.endsWith('"')) || (part.startsWith("'") && part.endsWith("'"))) {
// C'est une string littérale
result += part.slice(1, -1);
} else if (part.startsWith('str(') && part.endsWith(')')) {
// Conversion str() d'une variable ou expression
const varName = part.slice(4, -1).trim();
// Check si c'est une expression math simple (a + b)
if (varName.includes('+')) {
const [v1, v2] = varName.split('+').map(v => v.trim());
const val1 = variables[v1] !== undefined ? variables[v1] : parseInt(v1);
const val2 = variables[v2] !== undefined ? variables[v2] : parseInt(v2);
result += (val1 + val2);
} else {
result += variables[varName] !== undefined ? variables[varName] : `[Err: ${varName}]`;
}
} else {
// C'est une variable directe
result += variables[part] !== undefined ? variables[part] : '';
}
});
logs.push(result);
}
// Gestion simple des assignations (a = 5, b = "text")
else if (trimmed.includes('=')) {
const [varName, value] = trimmed.split('=').map(s => s.trim());
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
variables[varName] = value.slice(1, -1);
} else if (!isNaN(Number(value))) {
variables[varName] = Number(value);
}
}
});
if (logs.length === 0) logs.push("Code exécuté (simulation Python simplifiée).");
} catch (e) {
logs.push("Erreur de syntaxe (Simulée)");
}
setOutput(logs);
};
const handleRun = () => {
setIsRunning(true);
setOutput([]);
setTimeout(() => {
if (language === 'javascript') {
runJavaScript();
} else {
runPythonMock();
}
setIsRunning(false);
}, 500); // Petit délai pour l'effet "traitement"
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden border border-gray-200 dark:border-gray-700">
{/* Header */}
<div className="flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<div className="flex items-center gap-3">
<div className="bg-blue-100 dark:bg-blue-900/50 p-2 rounded-lg text-blue-600 dark:text-blue-400">
<Code2 size={24} />
</div>
<div>
<h3 className="font-bold text-lg text-gray-900 dark:text-white">Bac à Sable Code</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">Expérimentez en JavaScript ou Python</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex bg-gray-200 dark:bg-gray-700 rounded-lg p-1">
<button
onClick={() => setLanguage('javascript')}
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${language === 'javascript' ? 'bg-white dark:bg-gray-600 text-blue-600 dark:text-blue-300 shadow-sm' : 'text-gray-600 dark:text-gray-400'}`}
>
JavaScript
</button>
<button
onClick={() => setLanguage('python')}
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${language === 'python' ? 'bg-white dark:bg-gray-600 text-yellow-600 dark:text-yellow-400 shadow-sm' : 'text-gray-600 dark:text-gray-400'}`}
>
Python
</button>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-white transition-colors">
<X size={24} />
</button>
</div>
</div>
{/* Main Area */}
<div className="flex-grow flex flex-col md:flex-row overflow-hidden">
{/* Editor */}
<div className="md:w-1/2 flex flex-col border-r border-gray-200 dark:border-gray-700">
<div className="bg-gray-100 dark:bg-gray-800 px-4 py-2 text-xs font-mono text-gray-500 flex justify-between items-center border-b border-gray-200 dark:border-gray-700">
<span>éditeur.{language === 'javascript' ? 'js' : 'py'}</span>
<button
onClick={() => setCode(DEFAULT_CODE[language])}
className="flex items-center gap-1 hover:text-blue-500"
title="Réinitialiser"
>
<RefreshCw size={12} /> Reset
</button>
</div>
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
className="flex-grow w-full p-4 bg-white dark:bg-[#1e1e1e] text-gray-800 dark:text-blue-100 font-mono text-sm outline-none resize-none leading-relaxed"
spellCheck={false}
></textarea>
</div>
{/* Output */}
<div className="md:w-1/2 flex flex-col bg-gray-50 dark:bg-[#0d1117]">
<div className="bg-gray-200 dark:bg-gray-800 px-4 py-2 text-xs font-mono text-gray-500 flex justify-between items-center border-b border-gray-200 dark:border-gray-700">
<span className="flex items-center gap-2"><Terminal size={12} /> Console</span>
<button
onClick={() => setOutput([])}
className="hover:text-red-500"
title="Effacer"
>
<Trash2 size={12} />
</button>
</div>
<div className="flex-grow p-4 font-mono text-sm overflow-y-auto space-y-2">
{output.map((line, idx) => (
<div key={idx} className="text-gray-700 dark:text-gray-300 border-b border-gray-200/50 dark:border-gray-700/50 pb-1 last:border-0">
<span className="text-gray-400 mr-2 select-none">{'>'}</span>
{line}
</div>
))}
{output.length === 0 && (
<div className="text-gray-400 italic opacity-50 text-center mt-10">
Cliquez sur "Exécuter" pour voir le résultat ici.
</div>
)}
</div>
</div>
</div>
{/* Footer Controls */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 flex justify-between items-center">
<div className="text-xs text-gray-500">
{language === 'python' && (
<span className="flex items-center gap-1 text-yellow-600 dark:text-yellow-500">
<code className="bg-yellow-100 dark:bg-yellow-900/30 px-1 rounded">Mode simulation</code>
Supporte print(), variables et maths simples.
</span>
)}
</div>
<button
onClick={handleRun}
disabled={isRunning}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-2.5 rounded-xl font-bold shadow-lg hover:shadow-green-500/30 transition-all flex items-center gap-2 disabled:opacity-70 disabled:cursor-wait"
>
{isRunning ? <RefreshCw size={20} className="animate-spin" /> : <Play size={20} className="fill-current" />}
Exécuter le code
</button>
</div>
</div>
</div>
);
};
export default CodeSandboxModal;