Spaces:
Build error
Build error
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Trình biên dịch - UniCode</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/@phosphor-icons/web"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs/loader.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Be Vietnam Pro', sans-serif; | |
| background-color: #f8fafc; | |
| } | |
| .glass-panel { | |
| background: rgba(255, 255, 255, 0.95); | |
| border-right: 1px solid #e2e8f0; | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(156, 163, 175, 0.5); | |
| border-radius: 3px; | |
| } | |
| #editor-container { | |
| height: 100%; | |
| width: 100%; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-50 text-slate-800 h-screen flex overflow-hidden"> | |
| <!-- MOBILE OVERLAY BACKDROP --> | |
| <div id="sidebarBackdrop" onclick="toggleSidebar()" | |
| class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-40 hidden min-[1400px]:hidden transition-opacity"></div> | |
| <!-- SIDEBAR --> | |
| <aside id="sidebar" | |
| class="w-72 glass-panel h-full fixed min-[1400px]:static inset-y-0 left-0 z-50 flex flex-col transform -translate-x-full min-[1400px]:translate-x-0 transition-transform duration-300 shadow-xl min-[1400px]:shadow-lg bg-white"> | |
| <div class="p-6 flex items-center justify-between"> | |
| <div class="flex items-center gap-3 cursor-pointer" onclick="window.location.href='/'"> | |
| <div class="w-10 h-10 bg-indigo-600 rounded-xl flex items-center justify-center text-white shadow-lg"> | |
| <i class="ph-bold ph-code text-xl"></i> | |
| </div> | |
| <span class="font-bold text-xl text-slate-800">UniCode</span> | |
| </div> | |
| <button onclick="toggleSidebar()" | |
| class="min-[1400px]:hidden p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition"> | |
| <i class="ph-bold ph-x text-xl"></i> | |
| </button> | |
| </div> | |
| <div id="sidebarScrollContainer" class="flex-1 overflow-y-auto px-4 py-2"> | |
| <div class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3 pl-2">Chọn môn học</div> | |
| <nav id="sidebarList" class="space-y-1"> | |
| <div class="text-center text-sm p-4"><i class="ph ph-spinner animate-spin"></i> Đang tải menu...</div> | |
| </nav> | |
| </div> | |
| <div class="p-4 border-t border-slate-200"> | |
| <a href="/" | |
| class="flex items-center gap-3 px-4 py-3 text-slate-500 hover:text-indigo-600 transition rounded-xl"> | |
| <i class="ph ph-arrow-left text-lg"></i> Trang chủ | |
| </a> | |
| </div> | |
| </aside> | |
| <!-- CONTENT --> | |
| <div class="flex-1 flex flex-col h-full relative z-10 overflow-hidden"> | |
| <!-- Header --> | |
| <header class="h-16 border-b border-slate-200 bg-white flex items-center justify-between px-4 md:px-6 shrink-0 shadow-sm gap-3"> | |
| <div class="flex items-center gap-3 overflow-hidden"> | |
| <button onclick="toggleSidebar()" class="min-[1400px]:hidden p-2 -ml-2 text-slate-500 hover:bg-slate-100 rounded-lg"> | |
| <i class="ph-bold ph-list text-2xl"></i> | |
| </button> | |
| <h1 class="text-lg font-bold text-slate-800 flex items-center gap-2 truncate" id="pageTitle"> | |
| Loading... | |
| </h1> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <button id="runBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-bold px-6 py-2 rounded-lg transition flex items-center gap-2 shadow-lg shadow-indigo-200"> | |
| <i class="ph-bold ph-play"></i> RUN | |
| </button> | |
| </div> | |
| </header> | |
| <main class="flex-1 flex overflow-hidden"> | |
| <!-- File List (Left) --> | |
| <div class="w-full md:w-1/4 max-w-sm border-r border-slate-200 bg-white flex flex-col"> | |
| <div class="p-3 border-b border-slate-50 bg-slate-50/50 flex justify-between text-xs font-semibold text-slate-500 uppercase"> | |
| <span>Mã nguồn</span> | |
| <span>Ngôn ngữ</span> | |
| </div> | |
| <div class="flex-1 overflow-y-auto p-3 space-y-1" id="fileList"> | |
| <!-- Files will be loaded here --> | |
| </div> | |
| </div> | |
| <!-- Editor & Output (Right) --> | |
| <div class="flex-1 flex flex-col bg-slate-100 overflow-hidden"> | |
| <!-- Editor Container --> | |
| <div class="flex-1 relative border-b border-slate-200"> | |
| <div id="editor-container"></div> | |
| </div> | |
| <!-- Output Panel --> | |
| <div class="h-1/3 bg-[#1e1e1e] text-slate-300 font-mono text-sm overflow-hidden flex flex-col"> | |
| <div class="bg-[#252526] px-4 py-2 flex justify-between items-center border-b border-[#333]"> | |
| <span class="text-xs font-bold uppercase tracking-wider text-slate-500">Output</span> | |
| <button onclick="clearOutput()" class="text-slate-500 hover:text-white transition"> | |
| <i class="ph ph-trash"></i> | |
| </button> | |
| </div> | |
| <div id="output" class="flex-1 p-4 overflow-y-auto whitespace-pre-wrap"></div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| let editor; | |
| let currentFile; | |
| require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs' } }); | |
| require(['vs/editor/editor.main'], function () { | |
| editor = monaco.editor.create(document.getElementById('editor-container'), { | |
| value: '// Chọn một file để bắt đầu lập trình', | |
| language: 'javascript', | |
| theme: 'vs-dark', | |
| automaticLayout: true, | |
| fontSize: 14, | |
| fontFamily: "'Fira Code', 'Be Vietnam Pro', monospace", | |
| minimap: { enabled: false } | |
| }); | |
| }); | |
| document.addEventListener('DOMContentLoaded', async () => { | |
| const params = new URLSearchParams(window.location.search); | |
| const subjectId = params.get('id'); | |
| await loadSidebar(subjectId); | |
| if (subjectId) { | |
| loadSubjectDetails(subjectId); | |
| } else { | |
| document.getElementById('pageTitle').innerText = "Vui lòng chọn môn học"; | |
| } | |
| }); | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const backdrop = document.getElementById('sidebarBackdrop'); | |
| sidebar.classList.toggle('-translate-x-full'); | |
| backdrop.classList.toggle('hidden'); | |
| } | |
| async function loadSidebar(activeId) { | |
| try { | |
| const res = await fetch('data/subjects.json'); | |
| const subjects = await res.json(); | |
| const container = document.getElementById('sidebarList'); | |
| container.innerHTML = ''; | |
| // Simple render for demo | |
| subjects.forEach(sub => { | |
| const isActive = sub.id === activeId; | |
| const a = document.createElement('a'); | |
| a.href = `/subject?id=${sub.id}`; | |
| a.className = `block px-4 py-2 rounded-lg text-sm ${isActive ? 'bg-indigo-100 text-indigo-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`; | |
| a.innerText = sub.name; | |
| container.appendChild(a); | |
| }); | |
| } catch (e) { console.error(e); } | |
| } | |
| async function loadSubjectDetails(id) { | |
| try { | |
| const res = await fetch(`data/details/${id}.json`); | |
| const data = await res.json(); | |
| document.getElementById('pageTitle').innerText = data.name; | |
| const fileList = document.getElementById('fileList'); | |
| fileList.innerHTML = ''; | |
| data.files.forEach(file => { | |
| const div = document.createElement('div'); | |
| div.className = "p-3 rounded-lg border border-slate-100 hover:border-indigo-200 cursor-pointer flex justify-between items-center bg-white transition"; | |
| div.onclick = () => selectFile(file); | |
| div.innerHTML = ` | |
| <div class="flex items-center gap-2"> | |
| <i class="ph ph-file-code text-indigo-600"></i> | |
| <span class="text-sm font-medium">${file.name}</span> | |
| </div> | |
| <span class="text-[10px] bg-slate-100 px-1.5 py-0.5 rounded text-slate-500 uppercase">${file.type || 'code'}</span> | |
| `; | |
| fileList.appendChild(div); | |
| }); | |
| } catch (e) { console.error(e); } | |
| } | |
| async function selectFile(file) { | |
| currentFile = file; | |
| try { | |
| const res = await fetch(file.url); | |
| const code = await res.text(); | |
| let lang = 'javascript'; | |
| if (file.url.endsWith('.c')) lang = 'cpp'; | |
| if (file.url.endsWith('.java')) lang = 'java'; | |
| if (file.url.endsWith('.py')) lang = 'python'; | |
| monaco.editor.setModelLanguage(editor.getModel(), lang); | |
| editor.setValue(code); | |
| clearOutput(); | |
| document.getElementById('output').innerText = `Loaded ${file.name}\nReady to run...`; | |
| } catch (e) { | |
| console.error(e); | |
| editor.setValue(`// Error loading file: ${file.name}`); | |
| } | |
| } | |
| function clearOutput() { | |
| document.getElementById('output').innerText = ''; | |
| } | |
| document.getElementById('runBtn').onclick = async () => { | |
| if (!currentFile) { | |
| alert('Vui lòng chọn file trước!'); | |
| return; | |
| } | |
| const code = editor.getValue(); | |
| const output = document.getElementById('output'); | |
| output.innerText = 'Running...\n'; | |
| let language = 'python'; // default | |
| if (currentFile.url.endsWith('.c')) language = 'c'; | |
| if (currentFile.url.endsWith('.java')) language = 'java'; | |
| if (currentFile.url.endsWith('.py')) language = 'python'; | |
| try { | |
| const res = await fetch('http://localhost:3000/run', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ code, language }) | |
| }); | |
| const result = await res.json(); | |
| output.innerText = ''; | |
| if (result.stdout) output.innerText += result.stdout; | |
| if (result.stderr) { | |
| output.innerHTML += `<span class="text-red-400">${result.stderr}</span>`; | |
| } | |
| if (!result.stdout && !result.stderr) output.innerText = 'Program finished with no output.'; | |
| } catch (e) { | |
| output.innerText = `Error connecting to backend: ${e.message}\nMake sure server.js is running.`; | |
| } | |
| }; | |
| </script> | |
| </body> | |
| </html> |