Lashtw commited on
Commit
c1241f7
·
verified ·
1 Parent(s): 654e47b

Upload 10 files

Browse files
Files changed (1) hide show
  1. src/views/InstructorView.js +132 -0
src/views/InstructorView.js CHANGED
@@ -629,6 +629,138 @@ export function setupInstructorEvents() {
629
  renderTransposedHeatmap(users);
630
  }
631
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  // Create Room
633
  if (createBtn) {
634
  createBtn.addEventListener('click', async () => {
 
629
  renderTransposedHeatmap(users);
630
  }
631
 
632
+ // --- Transposed Heatmap Renderer ---
633
+ function renderTransposedHeatmap(users) {
634
+ const container = document.getElementById('heatmap-container');
635
+ if (!container) return;
636
+
637
+ // Make sure challenges are loaded (might be empty initially)
638
+ if (cachedChallenges.length === 0) {
639
+ container.innerHTML = '<div class="text-gray-500 text-center p-4">載入題目中...</div>';
640
+ return;
641
+ }
642
+
643
+ // Sort challenges by order
644
+ const challenges = cachedChallenges.sort((a, b) => a.order - b.order);
645
+
646
+ // Sort users by login time (or name)
647
+ const sortedUsers = users.sort((a, b) => (a.joinedAt || 0) - (b.joinedAt || 0));
648
+
649
+ let html = `
650
+ <div class="overflow-x-auto">
651
+ <table class="w-full text-left border-collapse">
652
+ <thead>
653
+ <tr>
654
+ <th class="p-3 border-b border-gray-700 bg-gray-800/50 sticky left-0 z-10 min-w-[150px]">
655
+ 學員 (${sortedUsers.length})
656
+ </th>
657
+ ${challenges.map(c => `
658
+ <th class="p-3 border-b border-gray-700 bg-gray-800/50 min-w-[120px] text-center relative group">
659
+ <div class="flex flex-col items-center">
660
+ <span class="text-sm font-bold text-cyan-400 whitespace-nowrap">${c.title}</span>
661
+ <div class="opacity-0 group-hover:opacity-100 transition-opacity absolute -top-8 bg-black text-white text-xs p-1 rounded">
662
+ ${c.description?.slice(0, 20)}...
663
+ </div>
664
+ <button onclick="window.analyzeChallenge('${c.id}', '${c.title}')"
665
+ class="mt-1 text-xs bg-purple-900/50 hover:bg-purple-600 text-purple-300 hover:text-white px-2 py-0.5 rounded border border-purple-700 transition-colors flex items-center gap-1">
666
+ <span>✨ AI 選粹</span>
667
+ </button>
668
+ </div>
669
+ </th>
670
+ `).join('')}
671
+ </tr>
672
+ </thead>
673
+ <tbody>
674
+ `;
675
+
676
+ sortedUsers.forEach(user => {
677
+ const isOnline = (Date.now() - (user.lastSeen || 0)) < 60000; // 1 min threshold
678
+ const statusDot = isOnline ? '<span class="text-green-500">●</span>' : '<span class="text-gray-600">●</span>';
679
+
680
+ html += `
681
+ <tr class="hover:bg-gray-800/30 transition-colors">
682
+ <td class="p-3 border-b border-gray-800 bg-gray-900/80 sticky left-0 z-10 font-mono text-sm border-r border-gray-700">
683
+ <div class="flex items-center justify-between group">
684
+ <div class="flex items-center space-x-2">
685
+ ${statusDot}
686
+ <span class="truncate max-w-[100px]" title="${user.nickname}">${user.nickname}</span>
687
+ </div>
688
+ <button onclick="window.confirmKick('${user.id}', '${user.nickname}')"
689
+ class="opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-400 p-1 text-xs"
690
+ title="踢出學員">✕</button>
691
+ </div>
692
+ </td>
693
+ `;
694
+
695
+ challenges.forEach(c => {
696
+ const progress = user.progress?.[c.id];
697
+ let cellContent = '<span class="text-gray-700">-</span>';
698
+ let cellClass = 'text-center border-b border-gray-800';
699
+
700
+ if (progress) {
701
+ if (progress.status === 'completed') {
702
+ cellContent = `
703
+ <div class="flex flex-col items-center cursor-pointer" onclick="window.showBroadcastModal('${user.id}', '${c.id}')">
704
+ <span class="text-green-500 text-xl">✓</span>
705
+ <span class="text-[10px] text-gray-500">${new Date(progress.completedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
706
+ </div>
707
+ `;
708
+ } else if (progress.status === 'failed') {
709
+ cellContent = '<span class="text-red-500 text-xl">✕</span>';
710
+ } else {
711
+ // In progress
712
+ cellContent = '<span class="text-yellow-500 animate-pulse">...</span>';
713
+ }
714
+ }
715
+
716
+ html += `<td class="${cellClass}">${cellContent}</td>`;
717
+ });
718
+
719
+ html += `</tr>`;
720
+ });
721
+
722
+ html += `
723
+ </tbody>
724
+ </table>
725
+ </div>
726
+ `;
727
+
728
+ container.innerHTML = html;
729
+ }
730
+
731
+ // Auto-Check Auth on Load
732
+ (async () => {
733
+ const { onAuthStateChanged } = await import("https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js");
734
+ const { auth } = await import("../services/firebase.js");
735
+
736
+ onAuthStateChanged(auth, async (user) => {
737
+ if (user) {
738
+ // User is signed in, check if instructor permission
739
+ try {
740
+ const instructorData = await checkInstructorPermission(user);
741
+ if (instructorData) {
742
+ authModal.classList.add('hidden');
743
+ checkPermissions(instructorData);
744
+ localStorage.setItem('vibecoding_instructor_name', instructorData.name);
745
+
746
+ // Auto-reconnect room if exists in localStorage
747
+ const savedRoom = localStorage.getItem('vibecoding_room_code');
748
+ if (savedRoom && !document.getElementById('dashboard-content').classList.contains('hidden')) {
749
+ // Already in dashboard, logic handled elsewhere or manual refresh needed
750
+ } else if (savedRoom) {
751
+ // Restore session logic if needed
752
+ // For now, let user click "Rejoin" or "Create".
753
+ // But wait, the user said "back button returns to login page".
754
+ // With this auth check, auth-modal will hide automatically.
755
+ }
756
+ }
757
+ } catch (e) {
758
+ console.error("Auth check failed:", e);
759
+ }
760
+ }
761
+ });
762
+ })();
763
+
764
  // Create Room
765
  if (createBtn) {
766
  createBtn.addEventListener('click', async () => {