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

Upload 10 files

Browse files
Files changed (1) hide show
  1. src/views/InstructorView.js +142 -109
src/views/InstructorView.js CHANGED
@@ -424,6 +424,116 @@ export function setupInstructorEvents() {
424
  }
425
  }
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  let roomUnsubscribe = null;
428
  let currentInstructor = null;
429
 
@@ -490,6 +600,37 @@ export function setupInstructorEvents() {
490
  }
491
  };
492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  // Email/Password Auth Logic
494
  if (loginBtn && registerBtn) {
495
  // Login Handler
@@ -618,115 +759,7 @@ export function setupInstructorEvents() {
618
 
619
  // Create Room
620
  if (createBtn) {
621
- // Dashboard Update Logic
622
- const updateDashboard = (data) => {
623
- const users = Array.isArray(data) ? data : (data?.users ? Object.values(data.users) : []);
624
-
625
- // Update global state for modals/snapshot
626
- currentStudents = users;
627
-
628
- // Use the full heatmap renderer
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 () => {
 
424
  }
425
  }
426
 
427
+ // --- Transposed Heatmap Renderer ---
428
+ function renderTransposedHeatmap(users) {
429
+ const container = document.getElementById('heatmap-container');
430
+ if (!container) return; // Might be hidden
431
+
432
+ // Make sure challenges are loaded (might be empty initially)
433
+ if (cachedChallenges.length === 0) {
434
+ container.innerHTML = '<div class="text-gray-500 text-center p-4">載入題目中...</div>';
435
+ return;
436
+ }
437
+
438
+ // Sort challenges by order
439
+ const challenges = cachedChallenges.sort((a, b) => a.order - b.order);
440
+
441
+ // Sort users by login time (or name)
442
+ const sortedUsers = users.sort((a, b) => (a.joinedAt || 0) - (b.joinedAt || 0));
443
+
444
+ let html = `
445
+ <div class="overflow-x-auto">
446
+ <table class="w-full text-left border-collapse">
447
+ <thead>
448
+ <tr>
449
+ <th class="p-3 border-b border-gray-700 bg-gray-800/50 sticky left-0 z-10 min-w-[150px]">
450
+ 學員 (${sortedUsers.length})
451
+ </th>
452
+ ${challenges.map(c => `
453
+ <th class="p-3 border-b border-gray-700 bg-gray-800/50 min-w-[120px] text-center relative group">
454
+ <div class="flex flex-col items-center">
455
+ <span class="text-sm font-bold text-cyan-400 whitespace-nowrap">${c.title}</span>
456
+ <div class="opacity-0 group-hover:opacity-100 transition-opacity absolute -top-8 bg-black text-white text-xs p-1 rounded z-20 w-48 text-center pointer-events-none">
457
+ ${c.description?.slice(0, 50)}...
458
+ </div>
459
+ <button onclick="window.analyzeChallenge('${c.id}', '${c.title}')"
460
+ 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 z-10 relative">
461
+ <span>✨ AI 選粹</span>
462
+ </button>
463
+ </div>
464
+ </th>
465
+ `).join('')}
466
+ </tr>
467
+ </thead>
468
+ <tbody>
469
+ `;
470
+
471
+ sortedUsers.forEach(user => {
472
+ const isOnline = (Date.now() - (user.lastSeen || 0)) < 60000; // 1 min threshold
473
+ const statusDot = isOnline ? '<span class="text-green-500">●</span>' : '<span class="text-gray-600">●</span>';
474
+
475
+ html += `
476
+ <tr class="hover:bg-gray-800/30 transition-colors">
477
+ <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">
478
+ <div class="flex items-center justify-between group">
479
+ <div class="flex items-center space-x-2">
480
+ ${statusDot}
481
+ <span class="truncate max-w-[100px]" title="${user.nickname}">${user.nickname}</span>
482
+ </div>
483
+ <button onclick="window.confirmKick('${user.id}', '${user.nickname}')"
484
+ class="opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-400 p-1 text-xs"
485
+ title="踢出學員">✕</button>
486
+ </div>
487
+ </td>
488
+ `;
489
+
490
+ challenges.forEach(c => {
491
+ const progress = user.progress?.[c.id];
492
+ let cellContent = '<span class="text-gray-700">-</span>';
493
+ let cellClass = 'text-center border-b border-gray-800 p-2';
494
+
495
+ if (progress) {
496
+ if (progress.status === 'completed') {
497
+ cellContent = `
498
+ <div class="flex flex-col items-center justify-center cursor-pointer hover:bg-gray-700/50 rounded p-1 transition-colors" onclick="window.showBroadcastModal('${user.id}', '${c.id}')">
499
+ <span class="text-green-500 text-xl">✓</span>
500
+ <span class="text-[10px] text-gray-500">${new Date(progress.completedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
501
+ </div>
502
+ `;
503
+ } else if (progress.status === 'failed') {
504
+ cellContent = '<span class="text-red-500 text-xl font-bold">✕</span>';
505
+ } else {
506
+ // In progress
507
+ cellContent = '<span class="text-yellow-500 animate-pulse font-bold">...</span>';
508
+ }
509
+ }
510
+
511
+ html += `<td class="${cellClass}">${cellContent}</td>`;
512
+ });
513
+
514
+ html += `</tr>`;
515
+ });
516
+
517
+ html += `
518
+ </tbody>
519
+ </table>
520
+ </div>
521
+ `;
522
+
523
+ container.innerHTML = html;
524
+ }
525
+
526
+ // Dashboard Update Logic (Global Scope)
527
+ const updateDashboard = (data) => {
528
+ const users = Array.isArray(data) ? data : (data?.users ? Object.values(data.users) : []);
529
+
530
+ // Update global state for modals/snapshot
531
+ currentStudents = users;
532
+
533
+ // Use the full heatmap renderer
534
+ renderTransposedHeatmap(users);
535
+ }
536
+
537
  let roomUnsubscribe = null;
538
  let currentInstructor = null;
539
 
 
600
  }
601
  };
602
 
603
+ // Auto-Check Auth on Load (Restores session from Firebase)
604
+ (async () => {
605
+ try {
606
+ const { onAuthStateChanged } = await import("https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js");
607
+ const { auth } = await import("../services/firebase.js");
608
+
609
+ onAuthStateChanged(auth, async (user) => {
610
+ if (user) {
611
+ // User is known to Firebase, check if they are an instructor
612
+ const instructorData = await checkInstructorPermission(user);
613
+ if (instructorData) {
614
+ authModal.classList.add('hidden');
615
+ checkPermissions(instructorData);
616
+ localStorage.setItem('vibecoding_instructor_name', instructorData.name);
617
+
618
+ // Auto-reconnect room if exists
619
+ const savedRoom = localStorage.getItem('vibecoding_room_code');
620
+ if (savedRoom) {
621
+ // If we already have a room code, we could auto-setup dashboard state here if needed
622
+ // For now, at least user is logged in
623
+ const displayRoomCode = document.getElementById('display-room-code');
624
+ if (displayRoomCode) displayRoomCode.textContent = savedRoom;
625
+ }
626
+ }
627
+ }
628
+ });
629
+ } catch (e) {
630
+ console.error("Auto-Auth Check Failed:", e);
631
+ }
632
+ })();
633
+
634
  // Email/Password Auth Logic
635
  if (loginBtn && registerBtn) {
636
  // Login Handler
 
759
 
760
  // Create Room
761
  if (createBtn) {
762
+ // Dashboard Update Logic moved to top scope
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
 
764
  // Auto-Check Auth on Load
765
  (async () => {