Vibecodingex / src /views /InstructorView.js
Lashtw's picture
Upload 8 files
41fc024 verified
raw
history blame
8.74 kB
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('');
}