Spaces:
Running
Running
| import { createRoom, subscribeToRoom, getChallenges } from "../services/classroom.js"; | |
| let cachedChallenges = []; | |
| export async function renderInstructorView() { | |
| // Pre-fetch challenges for table headers | |
| cachedChallenges = await getChallenges(); | |
| return ` | |
| <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur z-50 flex items-center justify-center"> | |
| <div class="bg-gray-800 p-8 rounded-xl border border-gray-600 shadow-2xl max-w-sm w-full"> | |
| <h2 class="text-xl font-bold text-center mb-6 text-white">🔒 講師身分驗證</h2> | |
| <input type="password" id="instructor-password" class="w-full bg-gray-900 border border-gray-700 rounded p-3 text-white text-center text-lg tracking-widest mb-4 focus:border-cyan-500 focus:outline-none" placeholder="輸入密碼"> | |
| <button id="auth-btn" class="w-full bg-purple-600 hover:bg-purple-500 text-white font-bold py-3 rounded-lg transition-colors">確認進入</button> | |
| </div> | |
| </div> | |
| <div class="min-h-screen p-6 pb-20"> | |
| <!-- Header --> | |
| <header class="flex flex-col md:flex-row justify-between items-center mb-10 bg-gray-800 bg-opacity-50 p-4 rounded-xl border border-gray-700 backdrop-blur-sm space-y-4 md:space-y-0"> | |
| <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600"> | |
| 講師儀表板 Instructor Dashboard | |
| </h1> | |
| <div id="room-info" class="hidden flex items-center space-x-4"> | |
| <span class="text-gray-400">教室代碼</span> | |
| <span id="display-room-code" class="text-3xl font-mono font-bold text-cyan-400 tracking-widest bg-gray-900 px-4 py-2 rounded-lg border border-cyan-500/30 shadow-[0_0_15px_rgba(34,211,238,0.3)]"></span> | |
| </div> | |
| <div class="flex space-x-3"> | |
| <button id="nav-admin-btn" class="bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition-all border border-gray-600"> | |
| 管理題目 (Admin) | |
| </button> | |
| <div id="create-room-container" class="flex items-center space-x-2"> | |
| <div class="flex items-center bg-gray-900 rounded-lg border border-gray-700 p-1"> | |
| <input type="text" id="rejoin-room-code" placeholder="輸入舊代碼" class="bg-transparent text-white px-2 py-1 w-24 text-center focus:outline-none text-sm"> | |
| <button id="rejoin-room-btn" class="bg-gray-700 hover:bg-gray-600 text-xs text-white px-2 py-1 rounded transition-colors"> | |
| 重回 | |
| </button> | |
| </div> | |
| <span class="text-gray-500">or</span> | |
| <button id="create-room-btn" class="bg-purple-600 hover:bg-purple-500 text-white font-bold py-2 px-6 rounded-lg transition-all shadow-lg shadow-purple-500/30"> | |
| 建立新教室 | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Student List --> | |
| <div id="dashboard-content" class="hidden"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" id="students-grid"> | |
| <!-- Student Cards will go here --> | |
| <div class="text-center text-gray-500 col-span-full py-20"> | |
| 等待學員加入... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| export function setupInstructorEvents() { | |
| // Auth Logic | |
| const authBtn = document.getElementById('auth-btn'); | |
| const pwdInput = document.getElementById('instructor-password'); | |
| const authModal = document.getElementById('auth-modal'); | |
| authBtn.addEventListener('click', () => checkPassword()); | |
| pwdInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') checkPassword(); | |
| }); | |
| function checkPassword() { | |
| if (pwdInput.value === '88300') { | |
| authModal.classList.add('hidden'); | |
| } else { | |
| alert('密碼錯誤'); | |
| pwdInput.value = ''; | |
| } | |
| } | |
| const createBtn = document.getElementById('create-room-btn'); | |
| const roomInfo = document.getElementById('room-info'); | |
| const createContainer = document.getElementById('create-room-container'); | |
| const dashboardContent = document.getElementById('dashboard-content'); | |
| const displayRoomCode = document.getElementById('display-room-code'); | |
| const studentsGrid = document.getElementById('students-grid'); | |
| const navAdminBtn = document.getElementById('nav-admin-btn'); | |
| navAdminBtn.addEventListener('click', () => { | |
| window.location.hash = 'admin'; | |
| }); | |
| // Auto-fill room code from local storage | |
| const savedRoomCode = localStorage.getItem('vibecoding_instructor_room'); | |
| if (savedRoomCode) { | |
| document.getElementById('rejoin-room-code').value = savedRoomCode; | |
| } | |
| const rejoinBtn = document.getElementById('rejoin-room-btn'); | |
| rejoinBtn.addEventListener('click', () => { | |
| const code = document.getElementById('rejoin-room-code').value.trim(); | |
| if (!code) return alert('請輸入教室代碼'); | |
| enterRoom(code); | |
| }); | |
| createBtn.addEventListener('click', async () => { | |
| try { | |
| createBtn.disabled = true; | |
| createBtn.textContent = "建立中..."; | |
| const roomCode = await createRoom(); | |
| enterRoom(roomCode); | |
| } catch (error) { | |
| console.error(error); | |
| alert("建立教室失敗"); | |
| createBtn.disabled = false; | |
| } | |
| }); | |
| function enterRoom(roomCode) { | |
| // UI Switch | |
| createContainer.classList.add('hidden'); | |
| roomInfo.classList.remove('hidden'); | |
| dashboardContent.classList.remove('hidden'); | |
| displayRoomCode.textContent = roomCode; | |
| // Save to local storage | |
| localStorage.setItem('vibecoding_instructor_room', roomCode); | |
| // Subscribe to updates | |
| subscribeToRoom(roomCode, (students) => { | |
| renderStudentCards(students, studentsGrid); | |
| }); | |
| } | |
| } | |
| function renderStudentCards(students, container) { | |
| if (students.length === 0) { | |
| container.innerHTML = '<div class="text-center text-gray-500 col-span-full py-20">尚無學員加入</div>'; | |
| return; | |
| } | |
| // Sort students by join time (if available) or random | |
| // students.sort((a,b) => a.joinedAt - b.joinedAt); | |
| container.innerHTML = students.map(student => { | |
| const progress = student.progress || {}; // Map of challengeId -> {status, prompt ...} | |
| // Progress Summary | |
| let totalCompleted = 0; | |
| let badgesHtml = cachedChallenges.map(c => { | |
| const isCompleted = progress[c.id]?.status === 'completed'; | |
| if (isCompleted) totalCompleted++; | |
| // Only show completed dots/badges or progress bar to save space? | |
| // User requested "Card showing status". 15 items is a lot for small badges. | |
| // Let's us simple dots color-coded by level. | |
| const colors = { beginner: 'cyan', intermediate: 'blue', advanced: 'purple' }; | |
| const color = colors[c.level] || 'gray'; | |
| return ` | |
| <div class="w-3 h-3 rounded-full ${isCompleted ? `bg-${color}-500 shadow-[0_0_5px_${color}]` : 'bg-gray-700'} | |
| title="${c.title} (${c.level})" | |
| ></div> | |
| `; | |
| }).join(''); | |
| return ` | |
| <div class="bg-gray-800 bg-opacity-40 backdrop-blur rounded-xl border border-gray-700 p-4 hover:border-gray-500 transition-all flex flex-col"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 rounded-full bg-gradient-to-br from-gray-700 to-gray-600 flex items-center justify-center text-lg font-bold text-white uppercase"> | |
| ${student.nickname[0]} | |
| </div> | |
| <div> | |
| <h3 class="font-bold text-white">${student.nickname}</h3> | |
| <p class="text-xs text-gray-400">完成度: ${totalCompleted} / ${cachedChallenges.length}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex flex-wrap gap-2 mt-auto"> | |
| ${badgesHtml} | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |