Lashtw commited on
Commit
55700d1
·
verified ·
1 Parent(s): 731c499

Upload 8 files

Browse files
Files changed (1) hide show
  1. src/views/InstructorView.js +87 -40
src/views/InstructorView.js CHANGED
@@ -191,76 +191,123 @@ export function setupInstructorEvents() {
191
  const now = new Date();
192
  dateEl.textContent = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`;
193
 
 
 
 
194
  container.innerHTML = '';
195
 
196
- // 1. Instructor Section (Center Top)
 
 
 
 
 
197
  const instructorSection = document.createElement('div');
198
- instructorSection.className = 'flex flex-col items-center justify-center mb-10 w-full relative';
199
  instructorSection.innerHTML = `
200
- <div class="relative group animate-bounce-slow">
201
- <div class="absolute inset-0 bg-yellow-500/20 blur-3xl rounded-full"></div>
202
  <!-- Pixel Art Avatar -->
203
- <img src="assets/instructor_avatar.png" class="relative w-40 h-40 md:w-56 md:h-56 object-contain pixel-art drop-shadow-[0_10px_20px_rgba(0,0,0,0.5)] z-10 hover:scale-105 transition-transform duration-300" alt="Instructor">
204
 
205
- <!-- Name Tag -->
206
- <div class="absolute -bottom-6 left-1/2 transform -translate-x-1/2 bg-black/80 backdrop-blur text-yellow-400 px-4 py-1.5 rounded-full border border-yellow-500/30 shadow-xl flex items-center space-x-2 z-20 whitespace-nowrap">
207
  <span class="text-xl">👑</span>
208
- <span class="font-bold text-lg">講師 (Instructor)</span>
 
 
 
 
209
  </div>
210
  </div>
211
  `;
212
- container.appendChild(instructorSection);
213
-
214
- // 2. Students Grid
215
- if (currentStudents.length === 0) {
216
- const emptyMsg = document.createElement('div');
217
- emptyMsg.className = 'text-gray-500 text-xl mt-10';
218
- emptyMsg.textContent = '尚無學員加入...';
219
- container.appendChild(emptyMsg);
220
- } else {
221
- const grid = document.createElement('div');
222
- grid.className = 'grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-6 md:gap-8 w-full px-4 md:px-10';
223
-
224
- currentStudents.forEach(s => {
 
 
 
 
 
 
225
  const progressMap = s.progress || {};
226
  const totalLikes = Object.values(progressMap).reduce((acc, p) => acc + (p.likes || 0), 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- // Get Monster
229
- const monster = getNextMonster(s.monster_stage || 0, totalLikes, currentStudents.length, s.monster_id);
 
 
 
230
 
231
  const card = document.createElement('div');
232
- card.className = 'flex flex-col items-center group relative';
 
 
 
 
 
 
 
 
 
233
  card.innerHTML = `
234
- <div class="relative w-24 h-24 flex items-center justify-center transform group-hover:-translate-y-2 transition-transform duration-300">
235
- <div class="w-20 h-20 md:w-24 md:h-24 pixel-art drop-shadow-lg filter group-hover:brightness-110 transition-all">
236
  ${generateMonsterSVG(monster)}
237
  </div>
238
-
239
- <!-- Floating Hearts Effect (Pseudo) -->
240
- <div class="absolute -top-2 -right-2 opacity-0 group-hover:opacity-100 transition-opacity text-pink-400 text-xs font-bold animate-pulse">
241
  ❤️
242
  </div>
243
  </div>
244
 
245
- <div class="mt-2 text-center bg-gray-800/60 backdrop-blur rounded-lg px-3 py-1.5 border border-gray-700 group-hover:border-cyan-500/50 transition-colors w-full max-w-[120px]">
246
- <div class="text-sm font-bold text-cyan-200 truncate">${s.nickname}</div>
247
- <div class="flex items-center justify-center space-x-2 mt-1">
248
- <span class="text-[10px] bg-blue-900/50 text-blue-300 px-1.5 rounded border border-blue-500/30">Lv.${(s.monster_stage || 0) + 1}</span>
249
- <div class="flex items-center text-[10px] text-pink-400 font-bold">
250
- <span>♥</span>
251
- <span class="ml-0.5">${totalLikes}</span>
252
- </div>
253
- </div>
254
  </div>
255
  `;
256
- grid.appendChild(card);
257
  });
258
- container.appendChild(grid);
259
  }
260
 
261
  modal.classList.remove('hidden');
262
  });
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  navAdminBtn.addEventListener('click', () => {
265
  // Save current room to return later
266
  const currentRoom = localStorage.getItem('vibecoding_instructor_room');
 
191
  const now = new Date();
192
  dateEl.textContent = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`;
193
 
194
+ // Get saved name
195
+ const savedName = localStorage.getItem('vibecoding_instructor_name') || '講師 (Instructor)';
196
+
197
  container.innerHTML = '';
198
 
199
+ // 1. Container for Relative Positioning
200
+ const relativeContainer = document.createElement('div');
201
+ relativeContainer.className = 'relative w-full h-[600px] md:h-[700px] overflow-hidden rounded-3xl bg-gray-900/50 border border-gray-700/30 shadow-inner';
202
+ container.appendChild(relativeContainer);
203
+
204
+ // 2. Instructor Section (Absolute Center)
205
  const instructorSection = document.createElement('div');
206
+ 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';
207
  instructorSection.innerHTML = `
208
+ <div class="relative">
209
+ <div class="absolute inset-0 bg-yellow-500/20 blur-3xl rounded-full animate-pulse"></div>
210
  <!-- Pixel Art Avatar -->
211
+ <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">
212
 
213
+ <!-- Editable Name Tag -->
214
+ <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 space-x-2 z-30 whitespace-nowrap group-hover:bg-black transition-colors">
215
  <span class="text-xl">👑</span>
216
+ <input type="text" id="instructor-name-input"
217
+ value="${savedName}"
218
+ 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 w-40 transition-all placeholder-yellow-700"
219
+ onclick="this.select()"
220
+ >
221
  </div>
222
  </div>
223
  `;
224
+ relativeContainer.appendChild(instructorSection);
225
+
226
+ // Save name on change
227
+ setTimeout(() => {
228
+ const input = document.getElementById('instructor-name-input');
229
+ if (input) {
230
+ input.addEventListener('input', (e) => {
231
+ localStorage.setItem('vibecoding_instructor_name', e.target.value);
232
+ });
233
+ }
234
+ }, 100);
235
+
236
+ // 3. Students Scatter
237
+ if (currentStudents.length > 0) {
238
+ // Randomize array to prevent fixed order bias
239
+ const students = [...currentStudents].sort(() => Math.random() - 0.5);
240
+ const total = students.length;
241
+
242
+ students.forEach((s, index) => {
243
  const progressMap = s.progress || {};
244
  const totalLikes = Object.values(progressMap).reduce((acc, p) => acc + (p.likes || 0), 0);
245
+ const monster = getNextMonster(s.monster_stage || 0, totalLikes, total, s.monster_id);
246
+
247
+ // Scatter Logic: Radial Distribution with Jitter
248
+ // Min radius ensures they don't cover instructor
249
+ const minR = 180; // slightly outside instructor (128 radius approx)
250
+ const maxR = 320; // max spread
251
+
252
+ // Angle: Evenly distributed + Jitter
253
+ const baseAngle = (index / total) * 2 * Math.PI; // Even separation
254
+ const angleJitter = (Math.random() - 0.5) * 0.5; // +/- ~15 degrees
255
+ const finalAngle = baseAngle + angleJitter;
256
+
257
+ // Radius: Random within range
258
+ const radius = minR + Math.random() * (maxR - minR);
259
 
260
+ // Convert to % for responsiveness (Relative to center 50%)
261
+ // Assuming container ~800px wide, 50% = 400px.
262
+ // x offset = cos(angle) * r
263
+ const xOff = Math.cos(finalAngle) * radius;
264
+ const yOff = Math.sin(finalAngle) * radius * 0.8; // Flatten Y slightly for perspective
265
 
266
  const card = document.createElement('div');
267
+ card.className = 'absolute flex flex-col items-center group/card z-10 hover:z-50 transition-all duration-500';
268
+
269
+ // Style placement
270
+ card.style.left = `calc(50% + ${xOff}px)`;
271
+ card.style.top = `calc(50% + ${yOff}px)`;
272
+ card.style.transform = 'translate(-50%, -50%)'; // Center pivot
273
+
274
+ // Add slight floating animation delay
275
+ const floatDelay = Math.random() * 2;
276
+
277
  card.innerHTML = `
278
+ <div class="relative w-20 h-20 md:w-24 md:h-24 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;">
279
+ <div class="w-full h-full pixel-art drop-shadow-md filter group-hover/card:brightness-110 transition-all">
280
  ${generateMonsterSVG(monster)}
281
  </div>
282
+ <div class="absolute -top-4 -right-2 opacity-0 group-hover/card:opacity-100 transition-opacity text-pink-400 text-lg font-bold animate-bounce hidden md:block">
 
 
283
  ❤️
284
  </div>
285
  </div>
286
 
287
+ <div class="mt-1 text-center bg-gray-900/40 backdrop-blur-sm rounded px-2 py-0.5 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">
288
+ <div class="text-xs font-bold text-cyan-200 shadow-black drop-shadow-md whitespace-nowrap">${s.nickname}</div>
 
 
 
 
 
 
 
289
  </div>
290
  `;
291
+ relativeContainer.appendChild(card);
292
  });
 
293
  }
294
 
295
  modal.classList.remove('hidden');
296
  });
297
 
298
+ // Add float animation style if not exists
299
+ if (!document.getElementById('anim-float')) {
300
+ const style = document.createElement('style');
301
+ style.id = 'anim-float';
302
+ style.innerHTML = `
303
+ @keyframes float {
304
+ 0%, 100% { transform: translateY(0) scale(1); }
305
+ 50% { transform: translateY(-5px) scale(1.02); }
306
+ }
307
+ `;
308
+ document.head.appendChild(style);
309
+ }
310
+
311
  navAdminBtn.addEventListener('click', () => {
312
  // Save current room to return later
313
  const currentRoom = localStorage.getItem('vibecoding_instructor_room');