Lashtw commited on
Commit
32956f2
·
verified ·
1 Parent(s): 03f1b1b

Upload 9 files

Browse files
Files changed (1) hide show
  1. 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
- <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>
27
  </div>
28
- </div>
29
 
30
- <!-- Broadcast Modal (Hidden by default) -->
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
- <!-- Group Photo Modal -->
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
- <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">
336
- ${dateStr} VibeCoding 怪獸成長營
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
- <div class="relative">
346
  <div class="absolute inset-0 bg-yellow-500/20 blur-3xl rounded-full animate-pulse"></div>
347
- <!-- Pixel Art Avatar -->
348
- <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">
349
-
350
- <!-- Editable Name Tag -->
351
- <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]">
352
- <span class="text-xl">👑</span>
353
- <input type="text" id="instructor-name-input"
354
- value="${savedName}"
355
- 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"
356
- style="width: ${Math.max(savedName.length * 20, 100)}px;"
357
- onclick="this.select()"
358
- oninput="this.style.width = Math.max(this.value.length * 20, 100) + 'px'"
359
- >
360
- </div>
361
- </div>
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
- const monster = getNextMonster(s.monster_stage || 0, totalLikes, total, s.monster_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <!-- Top Info: Monster Stats -->
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
- <!-- Monster Image -->
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
- <!-- Bottom Info: User Nickname -->
478
- <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">
479
- <div class="text-xs font-bold text-white shadow-black drop-shadow-md whitespace-nowrap tracking-wide">${s.nickname}</div>
480
- </div>
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
- @keyframes float {
495
- 0%, 100% { transform: translateY(0) scale(1); }
496
- 50% { transform: translateY(-5px) scale(1.02); }
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
- <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">
620
- <div class="flex justify-between items-end">
621
- <span class="text-sm text-gray-400">題目</span>
622
- <span class="text-sm text-cyan-400">學員 (${students.length})</span>
623
- </div>
624
- </th>
625
  `;
626
 
627
  students.forEach(student => {
628
  headerHtml += `
629
- <th class="p-2 text-center sticky top-0 bg-gray-800 z-20 border-b border-gray-700 min-w-[80px] group">
630
- <div class="flex flex-col items-center space-y-2 py-2">
631
- <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">
632
- ${student.nickname[0]}
633
- <!-- Online Indicator (Simulated) -->
634
- <div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-gray-800 rounded-full"></div>
635
- </div>
636
- <div class="flex items-center justify-center space-x-1">
637
- <span class="text-xs text-gray-300 font-medium truncate max-w-[80px] writing-vertical-lr" style="writing-mode: vertical-rl; text-orientation: mixed;">
638
- ${student.nickname}
639
- </span>
640
- <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="踢出學員">
641
- 🗑️
642
- </button>
643
- </div>
644
- </div>
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, '&quot;').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
- <td class="p-1 border border-gray-800/50 text-center align-middle h-14 hover:bg-white/5 transition-colors">
691
- <div class="mx-auto w-10 h-10 rounded-lg border flex items-center justify-center ${statusClass} transition-all duration-300" ${action}>
692
- ${content}
693
- </div>
694
- </td>
695
- `;
696
  }).join('');
697
 
698
  // Row Header (Challenge Title)
699
  return `
700
- <tr class="hover:bg-gray-800/50 transition-colors">
701
- <td class="p-3 sticky left-0 bg-gray-900 z-10 border-r border-b border-gray-700 shadow-md">
702
- <div class="flex items-center justify-between">
703
- <div class="flex flex-col">
704
- <span class="text-xs text-${color}-400 font-bold uppercase tracking-wider mb-0.5">${c.level}</span>
705
- <span class="font-bold text-white text-sm truncate max-w-[180px]" title="${c.title}">${index + 1}. ${c.title}</span>
706
- </div>
707
- <!-- Stats (Optional) -->
708
- <!-- <span class="text-xs text-gray-500">0%</span> -->
709
- </div>
710
- </td>
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, '&quot;').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