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

Upload 8 files

Browse files
src/views/InstructorView.js CHANGED
@@ -181,12 +181,9 @@ export function setupInstructorEvents() {
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
 
@@ -219,94 +216,111 @@ export function setupInstructorEvents() {
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('');
 
181
  displayRoomCode.textContent = roomCode;
182
  localStorage.setItem('vibecoding_instructor_room', roomCode);
183
 
184
+ // Subscribe to updates
 
 
 
185
  subscribeToRoom(roomCode, (students) => {
186
+ renderTransposedHeatmap(students);
187
  });
188
  }
189
 
 
216
  });
217
  }
218
 
219
+ /**
220
+ * Renders the Transposed Heatmap (Rows=Challenges, Cols=Students)
221
+ */
222
+ function renderTransposedHeatmap(students) {
223
+ const thead = document.getElementById('heatmap-header');
224
+ const tbody = document.getElementById('heatmap-body');
225
 
226
+ if (students.length === 0) {
227
+ thead.innerHTML = '<th class="p-4 text-left">等待資料...</th>';
228
+ tbody.innerHTML = '<tr><td class="p-10 text-center text-gray-500">尚無學員加入</td></tr>';
229
+ return;
230
+ }
231
 
232
+ // 1. Render Header (Students)
233
+ // Sticky Top for Header Row
234
+ // Sticky Left for the first cell ("Challenge/Student")
235
+ let headerHtml = `
236
+ <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">
237
+ <div class="flex justify-between items-end">
238
+ <span class="text-sm text-gray-400">題目</span>
239
+ <span class="text-sm text-cyan-400">學員 (${students.length})</span>
 
 
240
  </div>
241
+ </th>
242
+ `;
243
+
244
+ students.forEach(student => {
245
+ headerHtml += `
246
+ <th class="p-2 text-center sticky top-0 bg-gray-800 z-20 border-b border-gray-700 min-w-[80px] group">
247
+ <div class="flex flex-col items-center space-y-2 py-2">
248
+ <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">
249
+ ${student.nickname[0]}
250
+ <!-- Online Indicator (Simulated) -->
251
+ <div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-gray-800 rounded-full"></div>
252
+ </div>
253
+ <span class="text-xs text-gray-300 font-medium truncate max-w-[80px] writing-vertical-lr" style="writing-mode: vertical-rl; text-orientation: mixed;">
254
+ ${student.nickname}
255
+ </span>
256
+ </div>
257
+ </th>
258
  `;
 
259
  });
260
+ thead.innerHTML = headerHtml;
261
 
262
+ // 2. Render Body (Challenges as Rows)
263
+ if (cachedChallenges.length === 0) {
264
+ tbody.innerHTML = '<tr><td colspan="100" class="text-center py-4">沒有題目資料</td></tr>';
 
265
  return;
266
  }
267
 
268
+ tbody.innerHTML = cachedChallenges.map((c, index) => {
269
+ const colors = { beginner: 'cyan', intermediate: 'blue', advanced: 'purple' };
270
+ const color = colors[c.level] || 'gray';
271
 
272
+ // Build Row Cells (One per student)
273
+ const rowCells = students.map(student => {
274
  const p = student.progress?.[c.id];
275
+ let statusClass = 'bg-gray-800/30 border-gray-800'; // Default
276
  let content = '';
277
  let action = '';
278
 
279
  if (p) {
280
  if (p.status === 'completed') {
281
+ 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)]';
282
  content = '✅';
 
283
  const safePrompt = p.prompt.replace(/"/g, '&quot;').replace(/'/g, "\\'");
284
  action = `onclick="window.showBroadcastModal('${student.nickname}', '${c.title}', '${safePrompt}')"`;
285
  } else if (p.status === 'started') {
286
  // Check stuck
287
+ const startedAt = p.timestamp ? p.timestamp.toDate() : new Date();
288
  const now = new Date();
289
  const diffMins = (now - startedAt) / 1000 / 60;
290
 
291
  if (diffMins > 5) {
292
+ statusClass = 'bg-red-900/50 border-red-500 animate-pulse cursor-help';
293
  content = '🆘';
294
  } else {
295
+ statusClass = 'bg-blue-600/20 border-blue-500';
296
  content = '🔵';
297
  }
298
  }
 
 
 
299
  }
300
 
301
  return `
302
+ <td class="p-1 border border-gray-800/50 text-center align-middle h-14 hover:bg-white/5 transition-colors">
303
+ <div class="mx-auto w-10 h-10 rounded-lg border flex items-center justify-center ${statusClass} transition-all duration-300" ${action}>
304
  ${content}
305
  </div>
306
  </td>
307
  `;
308
  }).join('');
309
 
310
+ // Row Header (Challenge Title)
311
  return `
312
+ <tr class="hover:bg-gray-800/50 transition-colors">
313
+ <td class="p-3 sticky left-0 bg-gray-900 z-10 border-r border-b border-gray-700 shadow-md">
314
+ <div class="flex items-center justify-between">
315
+ <div class="flex flex-col">
316
+ <span class="text-xs text-${color}-400 font-bold uppercase tracking-wider mb-0.5">${c.level}</span>
317
+ <span class="font-bold text-white text-sm truncate max-w-[180px]" title="${c.title}">${index + 1}. ${c.title}</span>
318
+ </div>
319
+ <!-- Stats (Optional) -->
320
+ <!-- <span class="text-xs text-gray-500">0%</span> -->
321
  </div>
322
  </td>
323
+ ${rowCells}
324
  </tr>
325
  `;
326
  }).join('');
src/views/LandingView.js CHANGED
@@ -67,6 +67,8 @@ export function setupLandingEvents(navigateTo) {
67
  });
68
 
69
  instructorBtn.addEventListener('click', () => {
 
 
70
  navigateTo('instructor');
71
  });
72
  }
 
67
  });
68
 
69
  instructorBtn.addEventListener('click', () => {
70
+ // Clear any previous admin referer to ensure clean state
71
+ localStorage.removeItem('vibecoding_admin_referer');
72
  navigateTo('instructor');
73
  });
74
  }