Spaces:
Running
Running
Upload 9 files
Browse files- src/views/InstructorView.js +102 -90
src/views/InstructorView.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
import { createRoom, subscribeToRoom, getChallenges, resetProgress, removeUser } from "../services/classroom.js";
|
| 2 |
-
|
| 3 |
-
import { generateMonsterSVG, getNextMonster } from "../utils/monsterUtils.js";
|
| 4 |
|
| 5 |
// Load html-to-image dynamically (Better support than html2canvas)
|
| 6 |
const script = document.createElement('script');
|
|
@@ -19,15 +18,15 @@ export async function renderInstructorView() {
|
|
| 19 |
}
|
| 20 |
|
| 21 |
return `
|
| 22 |
-
<div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur z-50 flex items-center justify-center">
|
| 23 |
<div class="bg-gray-800 p-8 rounded-xl border border-gray-600 shadow-2xl max-w-sm w-full">
|
| 24 |
<h2 class="text-xl font-bold text-center mb-6 text-white">🔒 講師身分驗證</h2>
|
| 25 |
<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="輸入密碼">
|
| 26 |
-
|
| 27 |
</div>
|
| 28 |
-
</div>
|
| 29 |
|
| 30 |
-
<!--
|
| 31 |
<div id="broadcast-modal" class="fixed inset-0 bg-black/90 backdrop-blur z-50 hidden flex flex-col items-center justify-center p-8 transition-opacity duration-300">
|
| 32 |
<button onclick="closeBroadcast()" class="absolute top-6 right-6 text-gray-400 hover:text-white text-2xl">✕</button>
|
| 33 |
|
|
@@ -73,7 +72,7 @@ export async function renderInstructorView() {
|
|
| 73 |
</div>
|
| 74 |
</div>
|
| 75 |
|
| 76 |
-
<!--
|
| 77 |
<div id="group-photo-modal" class="fixed inset-0 bg-gray-900/95 backdrop-blur-md z-50 hidden flex flex-col items-center justify-center p-4 transition-opacity duration-300">
|
| 78 |
<button onclick="document.getElementById('group-photo-modal').classList.add('hidden')" class="absolute top-6 right-6 text-gray-400 hover:text-white text-4xl z-50">✕</button>
|
| 79 |
|
|
@@ -152,7 +151,7 @@ export async function renderInstructorView() {
|
|
| 152 |
</table>
|
| 153 |
</div>
|
| 154 |
</div>
|
| 155 |
-
|
| 156 |
}
|
| 157 |
|
| 158 |
export function setupInstructorEvents() {
|
|
@@ -311,7 +310,7 @@ export function setupInstructorEvents() {
|
|
| 311 |
|
| 312 |
// Update Date
|
| 313 |
const now = new Date();
|
| 314 |
-
dateEl.textContent = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`;
|
| 315 |
|
| 316 |
// Get saved name
|
| 317 |
const savedName = localStorage.getItem('vibecoding_instructor_name') || '講師 (Instructor)';
|
|
@@ -329,37 +328,37 @@ export function setupInstructorEvents() {
|
|
| 329 |
watermark.className = 'absolute bottom-2 right-2 md:bottom-4 md:right-4 z-[60] bg-black/70 backdrop-blur-sm rounded-lg px-4 py-2 pointer-events-none select-none border border-white/10 shadow-lg';
|
| 330 |
|
| 331 |
const d = new Date();
|
| 332 |
-
const dateStr = `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
|
| 333 |
|
| 334 |
watermark.innerHTML = `
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
</span>
|
| 338 |
-
|
| 339 |
relativeContainer.appendChild(watermark);
|
| 340 |
|
| 341 |
// 2. Instructor Section (Absolute Center)
|
| 342 |
const instructorSection = document.createElement('div');
|
| 343 |
instructorSection.className = 'absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center justify-center z-20 group cursor-pointer';
|
| 344 |
instructorSection.innerHTML = `
|
| 345 |
-
|
| 346 |
<div class="absolute inset-0 bg-yellow-500/20 blur-3xl rounded-full animate-pulse"></div>
|
| 347 |
-
<!--
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
relativeContainer.appendChild(instructorSection);
|
| 364 |
|
| 365 |
// Save name on change
|
|
@@ -393,7 +392,20 @@ export function setupInstructorEvents() {
|
|
| 393 |
students.forEach((s, index) => {
|
| 394 |
const progressMap = s.progress || {};
|
| 395 |
const totalLikes = Object.values(progressMap).reduce((acc, p) => acc + (p.likes || 0), 0);
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
|
| 398 |
// Scatter Logic: Radial Distribution with Jitter
|
| 399 |
// Min radius increased to verify clearance around label
|
|
@@ -448,14 +460,14 @@ export function setupInstructorEvents() {
|
|
| 448 |
const card = document.createElement('div');
|
| 449 |
card.className = 'absolute flex flex-col items-center group/card z-10 hover:z-50 transition-all duration-500';
|
| 450 |
|
| 451 |
-
card.style.left = `calc(50% + ${xOff}px)`;
|
| 452 |
-
card.style.top = `calc(50% + ${yOff}px)`;
|
| 453 |
card.style.transform = 'translate(-50%, -50%)';
|
| 454 |
|
| 455 |
const floatDelay = Math.random() * 2;
|
| 456 |
|
| 457 |
card.innerHTML = `
|
| 458 |
-
|
| 459 |
<div class="mb-1 text-center bg-gray-900/60 backdrop-blur-sm rounded-lg px-2 py-1 border border-gray-600/30 group-hover/card:bg-gray-800 group-hover/card:border-cyan-500/50 transition-all opacity-80 group-hover/card:opacity-100 transform translate-y-2 group-hover/card:translate-y-0 duration-300">
|
| 460 |
<div class="text-[10px] text-gray-300 mb-0.5 whitespace-nowrap">${monster.name.split(' ')[1] || monster.name}</div>
|
| 461 |
<div class="flex items-center justify-center space-x-2">
|
|
@@ -467,18 +479,18 @@ export function setupInstructorEvents() {
|
|
| 467 |
</div>
|
| 468 |
</div>
|
| 469 |
|
| 470 |
-
<!--
|
| 471 |
<div class="monster-img-container relative ${sizeClass} flex items-center justify-center transform group-hover/card:scale-125 transition-transform duration-300" style="animation: float 3s ease-in-out infinite; animation-delay: -${floatDelay}s;">
|
| 472 |
<div class="w-full h-full pixel-art drop-shadow-md filter group-hover/card:brightness-110 transition-all">
|
| 473 |
${generateMonsterSVG(monster)}
|
| 474 |
</div>
|
| 475 |
</div>
|
| 476 |
|
| 477 |
-
<!--
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
relativeContainer.appendChild(card);
|
| 483 |
});
|
| 484 |
}
|
|
@@ -491,11 +503,11 @@ export function setupInstructorEvents() {
|
|
| 491 |
const style = document.createElement('style');
|
| 492 |
style.id = 'anim-float';
|
| 493 |
style.innerHTML = `
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
document.head.appendChild(style);
|
| 500 |
}
|
| 501 |
|
|
@@ -616,34 +628,34 @@ function renderTransposedHeatmap(students) {
|
|
| 616 |
// Sticky Top for Header Row
|
| 617 |
// Sticky Left for the first cell ("Challenge/Student")
|
| 618 |
let headerHtml = `
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
</th>
|
| 625 |
`;
|
| 626 |
|
| 627 |
students.forEach(student => {
|
| 628 |
headerHtml += `
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
</th>
|
| 646 |
-
|
| 647 |
});
|
| 648 |
thead.innerHTML = headerHtml;
|
| 649 |
|
|
@@ -669,7 +681,7 @@ function renderTransposedHeatmap(students) {
|
|
| 669 |
statusClass = 'bg-green-500/20 border-green-500/50 hover:bg-green-500/40 cursor-pointer shadow-[0_0_10px_rgba(34,197,94,0.1)]';
|
| 670 |
content = '✅';
|
| 671 |
const safePrompt = p.prompt.replace(/"/g, '"').replace(/'/g, "\\'");
|
| 672 |
-
action = `onclick="window.showBroadcastModal('${student.id}', '${c.id}', '${student.nickname}', '${c.title}', '${safePrompt}')"`;
|
| 673 |
} else if (p.status === 'started') {
|
| 674 |
// Check stuck
|
| 675 |
const startedAt = p.timestamp ? p.timestamp.toDate() : new Date();
|
|
@@ -687,30 +699,30 @@ function renderTransposedHeatmap(students) {
|
|
| 687 |
}
|
| 688 |
|
| 689 |
return `
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
</td>
|
| 695 |
-
|
| 696 |
}).join('');
|
| 697 |
|
| 698 |
// Row Header (Challenge Title)
|
| 699 |
return `
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
${rowCells}
|
| 712 |
-
</tr>
|
| 713 |
-
|
| 714 |
}).join('');
|
| 715 |
}
|
| 716 |
|
|
|
|
| 1 |
import { createRoom, subscribeToRoom, getChallenges, resetProgress, removeUser } from "../services/classroom.js";
|
| 2 |
+
import { generateMonsterSVG, getNextMonster, MONSTER_DEFS } from "../utils/monsterUtils.js";
|
|
|
|
| 3 |
|
| 4 |
// Load html-to-image dynamically (Better support than html2canvas)
|
| 5 |
const script = document.createElement('script');
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
return `
|
| 21 |
+
< div id = "auth-modal" class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur z-50 flex items-center justify-center" >
|
| 22 |
<div class="bg-gray-800 p-8 rounded-xl border border-gray-600 shadow-2xl max-w-sm w-full">
|
| 23 |
<h2 class="text-xl font-bold text-center mb-6 text-white">🔒 講師身分驗證</h2>
|
| 24 |
<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="輸入密碼">
|
| 25 |
+
<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>
|
| 26 |
</div>
|
| 27 |
+
</div >
|
| 28 |
|
| 29 |
+
< !--Broadcast Modal(Hidden by default )-- >
|
| 30 |
<div id="broadcast-modal" class="fixed inset-0 bg-black/90 backdrop-blur z-50 hidden flex flex-col items-center justify-center p-8 transition-opacity duration-300">
|
| 31 |
<button onclick="closeBroadcast()" class="absolute top-6 right-6 text-gray-400 hover:text-white text-2xl">✕</button>
|
| 32 |
|
|
|
|
| 72 |
</div>
|
| 73 |
</div>
|
| 74 |
|
| 75 |
+
<!--Group Photo Modal-- >
|
| 76 |
<div id="group-photo-modal" class="fixed inset-0 bg-gray-900/95 backdrop-blur-md z-50 hidden flex flex-col items-center justify-center p-4 transition-opacity duration-300">
|
| 77 |
<button onclick="document.getElementById('group-photo-modal').classList.add('hidden')" class="absolute top-6 right-6 text-gray-400 hover:text-white text-4xl z-50">✕</button>
|
| 78 |
|
|
|
|
| 151 |
</table>
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
+
`;
|
| 155 |
}
|
| 156 |
|
| 157 |
export function setupInstructorEvents() {
|
|
|
|
| 310 |
|
| 311 |
// Update Date
|
| 312 |
const now = new Date();
|
| 313 |
+
dateEl.textContent = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')} `;
|
| 314 |
|
| 315 |
// Get saved name
|
| 316 |
const savedName = localStorage.getItem('vibecoding_instructor_name') || '講師 (Instructor)';
|
|
|
|
| 328 |
watermark.className = 'absolute bottom-2 right-2 md:bottom-4 md:right-4 z-[60] bg-black/70 backdrop-blur-sm rounded-lg px-4 py-2 pointer-events-none select-none border border-white/10 shadow-lg';
|
| 329 |
|
| 330 |
const d = new Date();
|
| 331 |
+
const dateStr = `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')} `;
|
| 332 |
|
| 333 |
watermark.innerHTML = `
|
| 334 |
+
< span class="text-lg md:text-2xl font-black font-mono bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-purple-400 tracking-wider" >
|
| 335 |
+
${dateStr} VibeCoding 怪獸成長營
|
| 336 |
+
</span >
|
| 337 |
+
`;
|
| 338 |
relativeContainer.appendChild(watermark);
|
| 339 |
|
| 340 |
// 2. Instructor Section (Absolute Center)
|
| 341 |
const instructorSection = document.createElement('div');
|
| 342 |
instructorSection.className = 'absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center justify-center z-20 group cursor-pointer';
|
| 343 |
instructorSection.innerHTML = `
|
| 344 |
+
< div class="relative" >
|
| 345 |
<div class="absolute inset-0 bg-yellow-500/20 blur-3xl rounded-full animate-pulse"></div>
|
| 346 |
+
<!--Pixel Art Avatar-- >
|
| 347 |
+
<img src="assets/instructor_avatar.png" class="relative w-48 h-48 md:w-64 md:h-64 object-contain pixel-art drop-shadow-[0_10px_30px_rgba(0,0,0,0.6)] z-10 hover:scale-105 transition-transform duration-300" alt="Instructor">
|
| 348 |
+
|
| 349 |
+
<!-- Editable Name Tag -->
|
| 350 |
+
<div class="absolute -bottom-8 left-1/2 transform -translate-x-1/2 bg-black/80 backdrop-blur text-yellow-400 px-6 py-2 rounded-full border border-yellow-500/30 shadow-2xl flex items-center justify-center space-x-2 z-30 whitespace-nowrap group-hover:bg-black transition-colors min-w-[150px] max-w-[300px]">
|
| 351 |
+
<span class="text-xl">👑</span>
|
| 352 |
+
<input type="text" id="instructor-name-input"
|
| 353 |
+
value="${savedName}"
|
| 354 |
+
class="bg-transparent border-b border-transparent hover:border-yellow-500/50 focus:border-yellow-500 text-lg font-bold text-yellow-400 text-center focus:outline-none transition-all placeholder-yellow-700"
|
| 355 |
+
style="width: ${Math.max(savedName.length * 20, 100)}px;"
|
| 356 |
+
onclick="this.select()"
|
| 357 |
+
oninput="this.style.width = Math.max(this.value.length * 20, 100) + 'px'"
|
| 358 |
+
>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
`;
|
| 362 |
relativeContainer.appendChild(instructorSection);
|
| 363 |
|
| 364 |
// Save name on change
|
|
|
|
| 392 |
students.forEach((s, index) => {
|
| 393 |
const progressMap = s.progress || {};
|
| 394 |
const totalLikes = Object.values(progressMap).reduce((acc, p) => acc + (p.likes || 0), 0);
|
| 395 |
+
|
| 396 |
+
// FIXED: Prioritize stored ID if valid (same as StudentView logic)
|
| 397 |
+
let monster;
|
| 398 |
+
if (s.monster_id && s.monster_id !== 'Egg' && s.monster_id !== 'Unknown') {
|
| 399 |
+
const stored = MONSTER_DEFS?.find(m => m.id === s.monster_id);
|
| 400 |
+
if (stored) {
|
| 401 |
+
monster = stored;
|
| 402 |
+
} else {
|
| 403 |
+
// Fallback if ID invalid
|
| 404 |
+
monster = getNextMonster(s.monster_stage || 0, totalLikes, total, s.monster_id);
|
| 405 |
+
}
|
| 406 |
+
} else {
|
| 407 |
+
monster = getNextMonster(s.monster_stage || 0, totalLikes, total, s.monster_id);
|
| 408 |
+
}
|
| 409 |
|
| 410 |
// Scatter Logic: Radial Distribution with Jitter
|
| 411 |
// Min radius increased to verify clearance around label
|
|
|
|
| 460 |
const card = document.createElement('div');
|
| 461 |
card.className = 'absolute flex flex-col items-center group/card z-10 hover:z-50 transition-all duration-500';
|
| 462 |
|
| 463 |
+
card.style.left = `calc(50 % + ${xOff}px)`;
|
| 464 |
+
card.style.top = `calc(50 % + ${yOff}px)`;
|
| 465 |
card.style.transform = 'translate(-50%, -50%)';
|
| 466 |
|
| 467 |
const floatDelay = Math.random() * 2;
|
| 468 |
|
| 469 |
card.innerHTML = `
|
| 470 |
+
< !--Top Info: Monster Stats-- >
|
| 471 |
<div class="mb-1 text-center bg-gray-900/60 backdrop-blur-sm rounded-lg px-2 py-1 border border-gray-600/30 group-hover/card:bg-gray-800 group-hover/card:border-cyan-500/50 transition-all opacity-80 group-hover/card:opacity-100 transform translate-y-2 group-hover/card:translate-y-0 duration-300">
|
| 472 |
<div class="text-[10px] text-gray-300 mb-0.5 whitespace-nowrap">${monster.name.split(' ')[1] || monster.name}</div>
|
| 473 |
<div class="flex items-center justify-center space-x-2">
|
|
|
|
| 479 |
</div>
|
| 480 |
</div>
|
| 481 |
|
| 482 |
+
<!--Monster Image-- >
|
| 483 |
<div class="monster-img-container relative ${sizeClass} flex items-center justify-center transform group-hover/card:scale-125 transition-transform duration-300" style="animation: float 3s ease-in-out infinite; animation-delay: -${floatDelay}s;">
|
| 484 |
<div class="w-full h-full pixel-art drop-shadow-md filter group-hover/card:brightness-110 transition-all">
|
| 485 |
${generateMonsterSVG(monster)}
|
| 486 |
</div>
|
| 487 |
</div>
|
| 488 |
|
| 489 |
+
<!--Bottom Info: User Nickname-- >
|
| 490 |
+
<div class="mt-1 text-center bg-black/60 backdrop-blur-sm rounded-full px-3 py-0.5 border border-gray-600/50 group-hover/card:bg-cyan-900/80 group-hover/card:border-cyan-400 transition-all">
|
| 491 |
+
<div class="text-xs font-bold text-white shadow-black drop-shadow-md whitespace-nowrap tracking-wide">${s.nickname}</div>
|
| 492 |
+
</div>
|
| 493 |
+
`;
|
| 494 |
relativeContainer.appendChild(card);
|
| 495 |
});
|
| 496 |
}
|
|
|
|
| 503 |
const style = document.createElement('style');
|
| 504 |
style.id = 'anim-float';
|
| 505 |
style.innerHTML = `
|
| 506 |
+
@keyframes float {
|
| 507 |
+
0 %, 100 % { transform: translateY(0) scale(1); }
|
| 508 |
+
50 % { transform: translateY(-5px) scale(1.02); }
|
| 509 |
+
}
|
| 510 |
+
`;
|
| 511 |
document.head.appendChild(style);
|
| 512 |
}
|
| 513 |
|
|
|
|
| 628 |
// Sticky Top for Header Row
|
| 629 |
// Sticky Left for the first cell ("Challenge/Student")
|
| 630 |
let headerHtml = `
|
| 631 |
+
< th class="p-3 text-left sticky left-0 top-0 bg-gray-800 z-30 border-b border-gray-600 min-w-[200px] border-r border-gray-700 shadow-md" >
|
| 632 |
+
<div class="flex justify-between items-end">
|
| 633 |
+
<span class="text-sm text-gray-400">題目</span>
|
| 634 |
+
<span class="text-sm text-cyan-400">學員 (${students.length})</span>
|
| 635 |
+
</div>
|
| 636 |
+
</th >
|
| 637 |
`;
|
| 638 |
|
| 639 |
students.forEach(student => {
|
| 640 |
headerHtml += `
|
| 641 |
+
< th class="p-2 text-center sticky top-0 bg-gray-800 z-20 border-b border-gray-700 min-w-[80px] group" >
|
| 642 |
+
<div class="flex flex-col items-center space-y-2 py-2">
|
| 643 |
+
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-gray-700 to-gray-600 flex items-center justify-center text-xs font-bold text-white uppercase border border-gray-500 shadow-sm relative">
|
| 644 |
+
${student.nickname[0]}
|
| 645 |
+
<!-- Online Indicator (Simulated) -->
|
| 646 |
+
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-gray-800 rounded-full"></div>
|
| 647 |
+
</div>
|
| 648 |
+
<div class="flex items-center justify-center space-x-1">
|
| 649 |
+
<span class="text-xs text-gray-300 font-medium truncate max-w-[80px] writing-vertical-lr" style="writing-mode: vertical-rl; text-orientation: mixed;">
|
| 650 |
+
${student.nickname}
|
| 651 |
+
</span>
|
| 652 |
+
<button onclick="window.confirmKick('${student.id}', '${student.nickname}')" class="text-gray-600 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity" title="踢出學員">
|
| 653 |
+
🗑️
|
| 654 |
+
</button>
|
| 655 |
+
</div>
|
| 656 |
+
</div>
|
| 657 |
+
</th >
|
| 658 |
+
`;
|
| 659 |
});
|
| 660 |
thead.innerHTML = headerHtml;
|
| 661 |
|
|
|
|
| 681 |
statusClass = 'bg-green-500/20 border-green-500/50 hover:bg-green-500/40 cursor-pointer shadow-[0_0_10px_rgba(34,197,94,0.1)]';
|
| 682 |
content = '✅';
|
| 683 |
const safePrompt = p.prompt.replace(/"/g, '"').replace(/'/g, "\\'");
|
| 684 |
+
action = `onclick = "window.showBroadcastModal('${student.id}', '${c.id}', '${student.nickname}', '${c.title}', '${safePrompt}')"`;
|
| 685 |
} else if (p.status === 'started') {
|
| 686 |
// Check stuck
|
| 687 |
const startedAt = p.timestamp ? p.timestamp.toDate() : new Date();
|
|
|
|
| 699 |
}
|
| 700 |
|
| 701 |
return `
|
| 702 |
+
< td class="p-1 border border-gray-800/50 text-center align-middle h-14 hover:bg-white/5 transition-colors" >
|
| 703 |
+
<div class="mx-auto w-10 h-10 rounded-lg border flex items-center justify-center ${statusClass} transition-all duration-300" ${action}>
|
| 704 |
+
${content}
|
| 705 |
+
</div>
|
| 706 |
+
</td >
|
| 707 |
+
`;
|
| 708 |
}).join('');
|
| 709 |
|
| 710 |
// Row Header (Challenge Title)
|
| 711 |
return `
|
| 712 |
+
< tr class="hover:bg-gray-800/50 transition-colors" >
|
| 713 |
+
<td class="p-3 sticky left-0 bg-gray-900 z-10 border-r border-b border-gray-700 shadow-md">
|
| 714 |
+
<div class="flex items-center justify-between">
|
| 715 |
+
<div class="flex flex-col">
|
| 716 |
+
<span class="text-xs text-${color}-400 font-bold uppercase tracking-wider mb-0.5">${c.level}</span>
|
| 717 |
+
<span class="font-bold text-white text-sm truncate max-w-[180px]" title="${c.title}">${index + 1}. ${c.title}</span>
|
| 718 |
+
</div>
|
| 719 |
+
<!-- Stats (Optional) -->
|
| 720 |
+
<!-- <span class="text-xs text-gray-500">0%</span> -->
|
| 721 |
+
</div>
|
| 722 |
+
</td>
|
| 723 |
${rowCells}
|
| 724 |
+
</tr >
|
| 725 |
+
`;
|
| 726 |
}).join('');
|
| 727 |
}
|
| 728 |
|