Lashtw commited on
Commit
d874f33
·
verified ·
1 Parent(s): 70d259b

Upload 8 files

Browse files
Files changed (2) hide show
  1. src/views/AdminView.js +7 -1
  2. src/views/InstructorView.js +224 -83
src/views/AdminView.js CHANGED
@@ -69,7 +69,13 @@ export function setupAdminEvents() {
69
  loadChallenges();
70
 
71
  document.getElementById('back-instructor-btn').addEventListener('click', () => {
72
- window.location.hash = 'instructor';
 
 
 
 
 
 
73
  });
74
 
75
  document.getElementById('add-challenge-btn').addEventListener('click', () => {
 
69
  loadChallenges();
70
 
71
  document.getElementById('back-instructor-btn').addEventListener('click', () => {
72
+ const referer = localStorage.getItem('vibecoding_admin_referer');
73
+ if (referer === 'instructor') {
74
+ window.location.hash = 'instructor';
75
+ } else {
76
+ window.location.hash = ''; // Main landing
77
+ }
78
+ localStorage.removeItem('vibecoding_admin_referer');
79
  });
80
 
81
  document.getElementById('add-challenge-btn').addEventListener('click', () => {
src/views/InstructorView.js CHANGED
@@ -4,7 +4,11 @@ let cachedChallenges = [];
4
 
5
  export async function renderInstructorView() {
6
  // Pre-fetch challenges for table headers
7
- cachedChallenges = await getChallenges();
 
 
 
 
8
 
9
  return `
10
  <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur z-50 flex items-center justify-center">
@@ -15,43 +19,95 @@ export async function renderInstructorView() {
15
  </div>
16
  </div>
17
 
18
- <div class="min-h-screen p-6 pb-20">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  <!-- Header -->
20
- <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">
21
- <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600">
22
- 講師儀表板 Instructor Dashboard
23
- </h1>
24
- <div id="room-info" class="hidden flex items-center space-x-4">
25
- <span class="text-gray-400">教室代碼</span>
26
- <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>
 
 
27
  </div>
 
28
  <div class="flex space-x-3">
 
 
 
 
 
 
 
29
  <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">
30
- 管理題目 (Admin)
31
  </button>
32
  <div id="create-room-container" class="flex items-center space-x-2">
33
- <div class="flex items-center bg-gray-900 rounded-lg border border-gray-700 p-1">
34
- <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">
35
- <button id="rejoin-room-btn" class="bg-gray-700 hover:bg-gray-600 text-xs text-white px-2 py-1 rounded transition-colors">
36
- 重回
37
- </button>
38
- </div>
39
- <span class="text-gray-500">or</span>
40
- <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">
41
- 建立新教室
42
- </button>
43
  </div>
44
  </div>
45
  </header>
46
 
47
- <!-- Student List -->
48
- <div id="dashboard-content" class="hidden">
49
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" id="students-grid">
50
- <!-- Student Cards will go here -->
51
- <div class="text-center text-gray-500 col-span-full py-20">
52
- 等待學員加入...
53
- </div>
54
- </div>
 
 
 
 
 
 
55
  </div>
56
  </div>
57
  `;
@@ -63,33 +119,36 @@ export function setupInstructorEvents() {
63
  const pwdInput = document.getElementById('instructor-password');
64
  const authModal = document.getElementById('auth-modal');
65
 
66
- authBtn.addEventListener('click', () => checkPassword());
67
- pwdInput.addEventListener('keypress', (e) => {
68
- if (e.key === 'Enter') checkPassword();
69
- });
70
-
71
- function checkPassword() {
72
  if (pwdInput.value === '88300') {
73
  authModal.classList.add('hidden');
74
  } else {
75
  alert('密碼錯誤');
76
  pwdInput.value = '';
77
  }
78
- }
 
 
 
 
 
79
 
80
  const createBtn = document.getElementById('create-room-btn');
81
  const roomInfo = document.getElementById('room-info');
82
  const createContainer = document.getElementById('create-room-container');
83
  const dashboardContent = document.getElementById('dashboard-content');
84
  const displayRoomCode = document.getElementById('display-room-code');
85
- const studentsGrid = document.getElementById('students-grid');
86
  const navAdminBtn = document.getElementById('nav-admin-btn');
87
 
88
  navAdminBtn.addEventListener('click', () => {
 
 
 
89
  window.location.hash = 'admin';
90
  });
91
 
92
- // Auto-fill room code from local storage
93
  const savedRoomCode = localStorage.getItem('vibecoding_instructor_room');
94
  if (savedRoomCode) {
95
  document.getElementById('rejoin-room-code').value = savedRoomCode;
@@ -99,92 +158,174 @@ export function setupInstructorEvents() {
99
  rejoinBtn.addEventListener('click', () => {
100
  const code = document.getElementById('rejoin-room-code').value.trim();
101
  if (!code) return alert('請輸入教室代碼');
102
-
103
  enterRoom(code);
104
  });
105
 
106
  createBtn.addEventListener('click', async () => {
107
  try {
108
  createBtn.disabled = true;
109
- createBtn.textContent = "建立中...";
110
-
111
  const roomCode = await createRoom();
112
  enterRoom(roomCode);
113
-
114
  } catch (error) {
115
  console.error(error);
116
- alert("建立教室失敗");
117
  createBtn.disabled = false;
118
  }
119
  });
120
 
121
  function enterRoom(roomCode) {
122
- // UI Switch
123
  createContainer.classList.add('hidden');
124
  roomInfo.classList.remove('hidden');
125
  dashboardContent.classList.remove('hidden');
126
  displayRoomCode.textContent = roomCode;
127
-
128
- // Save to local storage
129
  localStorage.setItem('vibecoding_instructor_room', roomCode);
130
 
131
- // Subscribe to updates
 
 
 
132
  subscribeToRoom(roomCode, (students) => {
133
- renderStudentCards(students, studentsGrid);
134
  });
135
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
 
138
- function renderStudentCards(students, container) {
 
139
  if (students.length === 0) {
140
- container.innerHTML = '<div class="text-center text-gray-500 col-span-full py-20">���無學員加入</div>';
141
  return;
142
  }
143
 
144
- // Sort students by join time (if available) or random
145
- // students.sort((a,b) => a.joinedAt - b.joinedAt);
146
-
147
- container.innerHTML = students.map(student => {
148
- const progress = student.progress || {}; // Map of challengeId -> {status, prompt ...}
149
 
150
- // Progress Summary
151
- let totalCompleted = 0;
152
- let badgesHtml = cachedChallenges.map(c => {
153
- const isCompleted = progress[c.id]?.status === 'completed';
154
- if (isCompleted) totalCompleted++;
 
155
 
156
- // Only show completed dots/badges or progress bar to save space?
157
- // User requested "Card showing status". 15 items is a lot for small badges.
158
- // Let's us simple dots color-coded by level.
 
 
 
 
 
 
 
 
 
159
 
160
- const colors = { beginner: 'cyan', intermediate: 'blue', advanced: 'purple' };
161
- const color = colors[c.level] || 'gray';
 
 
 
 
 
 
 
 
 
 
162
 
163
  return `
164
- <div class="w-3 h-3 rounded-full ${isCompleted ? `bg-${color}-500 shadow-[0_0_5px_${color}]` : 'bg-gray-700'}
165
- title="${c.title} (${c.level})"
166
- ></div>
 
 
167
  `;
168
  }).join('');
169
 
170
  return `
171
- <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">
172
- <div class="flex items-center justify-between mb-4">
173
- <div class="flex items-center space-x-3">
174
- <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">
175
- ${student.nickname[0]}
176
- </div>
177
- <div>
178
- <h3 class="font-bold text-white">${student.nickname}</h3>
179
- <p class="text-xs text-gray-400">完成度: ${totalCompleted} / ${cachedChallenges.length}</p>
180
  </div>
181
- </div>
182
- </div>
183
-
184
- <div class="flex flex-wrap gap-2 mt-auto">
185
- ${badgesHtml}
186
- </div>
187
- </div>
188
  `;
189
  }).join('');
190
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  export async function renderInstructorView() {
6
  // Pre-fetch challenges for table headers
7
+ try {
8
+ cachedChallenges = await getChallenges();
9
+ } catch (e) {
10
+ console.error("Failed header load", e);
11
+ }
12
 
13
  return `
14
  <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur z-50 flex items-center justify-center">
 
19
  </div>
20
  </div>
21
 
22
+ <!-- Broadcast Modal (Hidden by default) -->
23
+ <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">
24
+ <button onclick="closeBroadcast()" class="absolute top-6 right-6 text-gray-400 hover:text-white text-2xl">✕</button>
25
+
26
+ <div id="broadcast-content" class="bg-gray-800 border border-gray-600 rounded-2xl p-8 max-w-4xl w-full text-center shadow-2xl transform transition-transform scale-95 opacity-0">
27
+ <div class="mb-4 flex flex-col items-center">
28
+ <div class="w-16 h-16 rounded-full bg-cyan-600 flex items-center justify-center text-3xl font-bold text-white mb-2" id="broadcast-avatar">
29
+ D
30
+ </div>
31
+ <h3 class="text-xl text-cyan-300 font-bold" id="broadcast-author">Dave</h3>
32
+ <span class="text-gray-500 text-sm" id="broadcast-challenge">Challenge Name</span>
33
+ </div>
34
+
35
+ <div class="bg-black/30 rounded-xl p-6 mb-8 text-left overflow-auto max-h-[50vh]">
36
+ <pre class="text-green-400 font-mono text-lg whitespace-pre-wrap" id="broadcast-prompt">Loading...</pre>
37
+ </div>
38
+
39
+ <div class="flex justify-center space-x-4">
40
+ <button id="btn-show-stage" class="bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg">
41
+ <span>🖥️ 投放到大螢幕 (本機)</span>
42
+ </button>
43
+ <!-- Future Feature: Send to Students -->
44
+ <!--
45
+ <button id="btn-broadcast-all" class="bg-purple-600 hover:bg-purple-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg opacity-50 cursor-not-allowed" title="此功能開發中">
46
+ <span>📡 推送給所有人</span>
47
+ </button>
48
+ -->
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Big Screen Mode (Initially hidden inside modal) -->
53
+ <div id="stage-view" class="hidden absolute inset-0 bg-gray-900 flex flex-col items-center justify-center p-10">
54
+ <button onclick="closeStage()" class="absolute top-6 right-6 text-gray-500 hover:text-white text-4xl">✕</button>
55
+ <h1 class="text-4xl font-bold text-cyan-400 mb-8" id="stage-title">優秀作品展示</h1>
56
+ <div class="bg-black border-2 border-cyan-500/50 rounded-2xl p-10 max-w-6xl w-full shadow-[0_0_50px_rgba(6,182,212,0.2)]">
57
+ <pre class="text-3xl text-green-400 font-mono whitespace-pre-wrap leading-relaxed" id="stage-prompt">...</pre>
58
+ </div>
59
+ <div class="mt-8 text-2xl text-gray-400">
60
+ Author: <span class="text-white font-bold" id="stage-author">Dave</span>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
66
  <!-- Header -->
67
+ <header class="flex flex-col md:flex-row justify-between items-center mb-6 bg-gray-800 p-4 rounded-xl border border-gray-700 space-y-4 md:space-y-0 sticky top-0 z-30 shadow-lg">
68
+ <div class="flex items-center space-x-4">
69
+ <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600">
70
+ 儀表板
71
+ </h1>
72
+ <div id="room-info" class="hidden flex items-center space-x-2 bg-black/30 px-3 py-1 rounded-lg border border-gray-700">
73
+ <span class="text-xs text-gray-500 uppercase">Room</span>
74
+ <span id="display-room-code" class="text-xl font-mono font-bold text-cyan-400 tracking-widest"></span>
75
+ </div>
76
  </div>
77
+
78
  <div class="flex space-x-3">
79
+ <div class="flex items-center space-x-2 text-xs text-gray-400 mr-4 border-r border-gray-700 pr-4">
80
+ <div class="flex items-center"><div class="w-3 h-3 bg-gray-700 rounded-sm mr-1"></div> 未開始</div>
81
+ <div class="flex items-center"><div class="w-3 h-3 bg-blue-600 rounded-sm mr-1"></div> 進行中</div>
82
+ <div class="flex items-center"><div class="w-3 h-3 bg-green-500 rounded-sm mr-1"></div> 已完成</div>
83
+ <div class="flex items-center"><div class="w-3 h-3 bg-red-500 animate-pulse rounded-sm mr-1"></div> 卡關 (>5m)</div>
84
+ </div>
85
+
86
  <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">
87
+ 管理題目
88
  </button>
89
  <div id="create-room-container" class="flex items-center space-x-2">
90
+ <input type="text" id="rejoin-room-code" placeholder="代碼" class="bg-gray-900 border border-gray-700 text-white px-3 py-2 rounded-lg w-20 text-center focus:outline-none focus:border-cyan-500">
91
+ <button id="rejoin-room-btn" class="bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded-lg">重回</button>
92
+ <button id="create-room-btn" class="bg-purple-600 hover:bg-purple-500 text-white font-bold px-4 py-2 rounded-lg shadow-lg">開房</button>
 
 
 
 
 
 
 
93
  </div>
94
  </div>
95
  </header>
96
 
97
+ <!-- Heatmap Content -->
98
+ <div id="dashboard-content" class="hidden overflow-x-auto pb-10">
99
+ <table class="w-full border-collapse">
100
+ <thead>
101
+ <tr id="heatmap-header">
102
+ <th class="p-3 text-left sticky left-0 bg-gray-800 z-20 border-b border-gray-700 min-w-[150px]">學員 / 關卡</th>
103
+ <!-- Challenges headers generated dynamically -->
104
+ </tr>
105
+ </thead>
106
+ <tbody id="heatmap-body">
107
+ <!-- Rows generated dynamically -->
108
+ <tr><td colspan="100" class="text-center py-10 text-gray-500">等待資料載入...</td></tr>
109
+ </tbody>
110
+ </table>
111
  </div>
112
  </div>
113
  `;
 
119
  const pwdInput = document.getElementById('instructor-password');
120
  const authModal = document.getElementById('auth-modal');
121
 
122
+ // Default password check
123
+ const checkPassword = () => {
 
 
 
 
124
  if (pwdInput.value === '88300') {
125
  authModal.classList.add('hidden');
126
  } else {
127
  alert('密碼錯誤');
128
  pwdInput.value = '';
129
  }
130
+ };
131
+
132
+ authBtn.addEventListener('click', checkPassword);
133
+ pwdInput.addEventListener('keypress', (e) => {
134
+ if (e.key === 'Enter') checkPassword();
135
+ });
136
 
137
  const createBtn = document.getElementById('create-room-btn');
138
  const roomInfo = document.getElementById('room-info');
139
  const createContainer = document.getElementById('create-room-container');
140
  const dashboardContent = document.getElementById('dashboard-content');
141
  const displayRoomCode = document.getElementById('display-room-code');
 
142
  const navAdminBtn = document.getElementById('nav-admin-btn');
143
 
144
  navAdminBtn.addEventListener('click', () => {
145
+ // Save current room to return later
146
+ const currentRoom = localStorage.getItem('vibecoding_instructor_room');
147
+ localStorage.setItem('vibecoding_admin_referer', 'instructor'); // track entry source
148
  window.location.hash = 'admin';
149
  });
150
 
151
+ // Auto-fill code
152
  const savedRoomCode = localStorage.getItem('vibecoding_instructor_room');
153
  if (savedRoomCode) {
154
  document.getElementById('rejoin-room-code').value = savedRoomCode;
 
158
  rejoinBtn.addEventListener('click', () => {
159
  const code = document.getElementById('rejoin-room-code').value.trim();
160
  if (!code) return alert('請輸入教室代碼');
 
161
  enterRoom(code);
162
  });
163
 
164
  createBtn.addEventListener('click', async () => {
165
  try {
166
  createBtn.disabled = true;
167
+ createBtn.textContent = "...";
 
168
  const roomCode = await createRoom();
169
  enterRoom(roomCode);
 
170
  } catch (error) {
171
  console.error(error);
172
+ alert("建立失敗");
173
  createBtn.disabled = false;
174
  }
175
  });
176
 
177
  function enterRoom(roomCode) {
 
178
  createContainer.classList.add('hidden');
179
  roomInfo.classList.remove('hidden');
180
  dashboardContent.classList.remove('hidden');
181
  displayRoomCode.textContent = roomCode;
 
 
182
  localStorage.setItem('vibecoding_instructor_room', roomCode);
183
 
184
+ // Update headers first
185
+ renderHeatmapHeaders();
186
+
187
+ // Subscribe
188
  subscribeToRoom(roomCode, (students) => {
189
+ renderHeatmapBody(students);
190
  });
191
  }
192
+
193
+ // Modal Events
194
+ window.closeBroadcast = () => {
195
+ const modal = document.getElementById('broadcast-modal');
196
+ const content = document.getElementById('broadcast-content');
197
+ content.classList.remove('opacity-100', 'scale-100');
198
+ content.classList.add('scale-95', 'opacity-0');
199
+ setTimeout(() => modal.classList.add('hidden'), 300);
200
+ };
201
+
202
+ window.openStage = (prompt, author) => {
203
+ document.getElementById('broadcast-content').classList.add('hidden');
204
+ const stage = document.getElementById('stage-view');
205
+ stage.classList.remove('hidden');
206
+ document.getElementById('stage-prompt').textContent = prompt;
207
+ document.getElementById('stage-author').textContent = author;
208
+ };
209
+
210
+ window.closeStage = () => {
211
+ document.getElementById('stage-view').classList.add('hidden');
212
+ document.getElementById('broadcast-content').classList.remove('hidden');
213
+ };
214
+
215
+ document.getElementById('btn-show-stage').addEventListener('click', () => {
216
+ const prompt = document.getElementById('broadcast-prompt').textContent;
217
+ const author = document.getElementById('broadcast-author').textContent;
218
+ window.openStage(prompt, author);
219
+ });
220
+ }
221
+
222
+ function renderHeatmapHeaders() {
223
+ const headerRow = document.getElementById('heatmap-header');
224
+ // Keep first col
225
+ headerRow.innerHTML = '<th class="p-4 text-left sticky left-0 bg-gray-800 z-20 border-b border-gray-600 min-w-[200px] text-gray-300 font-bold">學員名單</th>';
226
+
227
+ cachedChallenges.forEach((c, index) => {
228
+ const colors = { beginner: 'cyan', intermediate: 'blue', advanced: 'purple' };
229
+ const color = colors[c.level] || 'gray';
230
+
231
+ const th = document.createElement('th');
232
+ th.className = `p-2 text-center min-w-[60px] border-b border-gray-700 relative group cursor-help`;
233
+ th.innerHTML = `
234
+ <div class="flex flex-col items-center">
235
+ <span class="text-xs text-${color}-400 font-bold uppercase tracking-wider mb-1">${c.level[0].toUpperCase()}${index + 1}</span>
236
+ <div class="w-1 h-4 bg-${color}-500/50 rounded-full"></div>
237
+ </div>
238
+ <!-- Tooltip -->
239
+ <div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 bg-black text-white text-xs p-2 rounded hidden group-hover:block z-50 pointer-events-none">
240
+ ${c.title}
241
+ </div>
242
+ `;
243
+ headerRow.appendChild(th);
244
+ });
245
  }
246
 
247
+ function renderHeatmapBody(students) {
248
+ const tbody = document.getElementById('heatmap-body');
249
  if (students.length === 0) {
250
+ tbody.innerHTML = '<tr><td colspan="100" class="text-center py-10 text-gray-500">無學員加入</td></tr>';
251
  return;
252
  }
253
 
254
+ // Sort: Online first (based on last_active maybe?), then name
255
+ // students.sort((a,b) => ...);
 
 
 
256
 
257
+ tbody.innerHTML = students.map(student => {
258
+ const cells = cachedChallenges.map(c => {
259
+ const p = student.progress?.[c.id];
260
+ let statusClass = 'bg-gray-800/50 border-gray-700'; // Default gray
261
+ let content = '';
262
+ let action = '';
263
 
264
+ if (p) {
265
+ if (p.status === 'completed') {
266
+ statusClass = 'bg-green-500/20 border-green-500/50 hover:bg-green-500/40 cursor-pointer';
267
+ content = '✅';
268
+ // Escaped prompt for safety
269
+ const safePrompt = p.prompt.replace(/"/g, '&quot;').replace(/'/g, "\\'");
270
+ action = `onclick="window.showBroadcastModal('${student.nickname}', '${c.title}', '${safePrompt}')"`;
271
+ } else if (p.status === 'started') {
272
+ // Check stuck
273
+ const startedAt = p.timestamp ? p.timestamp.toDate() : new Date(); // Firestore timestamp to Date
274
+ const now = new Date();
275
+ const diffMins = (now - startedAt) / 1000 / 60;
276
 
277
+ if (diffMins > 5) {
278
+ statusClass = 'bg-red-900/50 border-red-500 animate-pulse'; // Red Flashing
279
+ content = '🆘';
280
+ } else {
281
+ statusClass = 'bg-blue-600/30 border-blue-500'; // Blue
282
+ content = '🔵';
283
+ }
284
+ }
285
+ } else {
286
+ // Not started
287
+ statusClass = 'bg-gray-800/30 border-gray-800';
288
+ }
289
 
290
  return `
291
+ <td class="p-2 border border-gray-700/50 text-center align-middle relative h-16">
292
+ <div class="w-full h-full rounded-lg border flex items-center justify-center ${statusClass} transition-colors" ${action}>
293
+ ${content}
294
+ </div>
295
+ </td>
296
  `;
297
  }).join('');
298
 
299
  return `
300
+ <tr class="hover:bg-gray-800/30 transition-colors group">
301
+ <td class="p-4 sticky left-0 bg-gray-900 z-10 border-r border-gray-700 group-hover:bg-gray-800 transition-colors">
302
+ <div class="flex items-center space-x-3">
303
+ <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">
304
+ ${student.nickname[0]}
305
+ </div>
306
+ <span class="font-bold text-gray-300 text-sm truncate max-w-[120px]">${student.nickname}</span>
 
 
307
  </div>
308
+ </td>
309
+ ${cells}
310
+ </tr>
 
 
 
 
311
  `;
312
  }).join('');
313
  }
314
+
315
+ // Global scope for HTML access
316
+ window.showBroadcastModal = (nickname, title, prompt) => {
317
+ const modal = document.getElementById('broadcast-modal');
318
+ const content = document.getElementById('broadcast-content');
319
+
320
+ document.getElementById('broadcast-avatar').textContent = nickname[0];
321
+ document.getElementById('broadcast-author').textContent = nickname;
322
+ document.getElementById('broadcast-challenge').textContent = title;
323
+ document.getElementById('broadcast-prompt').textContent = prompt;
324
+
325
+ modal.classList.remove('hidden');
326
+ // Animation trigger
327
+ setTimeout(() => {
328
+ content.classList.remove('scale-95', 'opacity-0');
329
+ content.classList.add('opacity-100', 'scale-100');
330
+ }, 10);
331
+ };