Lashtw commited on
Commit
6aa5fff
·
verified ·
1 Parent(s): 5d47b59

Upload 9 files

Browse files
Files changed (1) hide show
  1. src/views/StudentView.js +53 -30
src/views/StudentView.js CHANGED
@@ -1,5 +1,5 @@
1
  import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserMonster, getUser, subscribeToUserProgress } from "../services/classroom.js";
2
- import { generateMonsterSVG, getNextMonster, MONSTER_STAGES } from "../utils/monsterUtils.js";
3
 
4
 
5
  // Cache challenges locally
@@ -198,9 +198,12 @@ export async function renderStudentView() {
198
  }
199
 
200
  return `
201
- <div id="monster-container-fixed" class="fixed top-24 left-1/2 -translate-x-1/2 z-50 flex flex-col items-center group pointer-events-none sm:pointer-events-auto w-40 h-40">
202
- <!-- Walking Container -->
203
- <div class="pixel-art-container relative transform transition-transform duration-500 ease-out origin-center hover:scale-110"
 
 
 
204
  style="transform: scale(${currentScale}); animation: walk-float 6s ease-in-out infinite;">
205
 
206
  <div class="pixel-monster w-28 h-28 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out;">
@@ -213,8 +216,9 @@ export async function renderStudentView() {
213
  </div>
214
  </div>
215
 
216
- <!-- Evolution Prompt -->
217
- ${canEvolve ? `
 
218
  <div id="evolution-prompt" class="absolute top-full mt-2 pointer-events-auto animate-bounce z-50">
219
  <div class="flex flex-col items-center">
220
  <div class="bg-gray-900/90 text-pink-200 text-xs py-2 px-3 rounded-xl border border-pink-500/30 shadow-lg text-center font-bold mb-1 backdrop-blur-sm whitespace-nowrap">
@@ -226,9 +230,10 @@ export async function renderStudentView() {
226
  </button>
227
  </div>
228
  </div>
229
- ` : ''}
 
230
 
231
- <!-- Stats Tooltip -->
232
  <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-slate-300 p-3 rounded-xl border border-slate-700 mt-6 text-left pointer-events-auto shadow-2xl min-w-[120px]">
233
  <div class="font-bold text-white text-sm mb-1 text-center border-b border-gray-700 pb-1">${monster.name}</div>
234
  <div class="space-y-1 mt-1">
@@ -237,20 +242,20 @@ export async function renderStudentView() {
237
  <div class="flex justify-between"><span>⚔️ 任務:</span> <span class="text-yellow-400 font-bold">${totalCompleted}</span></div>
238
  </div>
239
  </div>
240
- </div>
241
 
242
  <style>
243
  @keyframes breathe {
244
- 0%, 100% { transform: translateY(0); filter: brightness(1); }
245
- 50% { transform: translateY(-3px); filter: brightness(1.1); }
246
  }
247
  @keyframes walk-float {
248
- 0%, 100% { transform: translateX(0) scale(${currentScale}); }
249
- 25% { transform: translateX(-10px) rotate(-2deg) scale(${currentScale}); }
250
- 75% { transform: translateX(10px) rotate(2deg) scale(${currentScale}); }
251
  }
252
  </style>
253
- `;
254
  };
255
 
256
  // Inject Initial Monster UI
@@ -287,7 +292,7 @@ export async function renderStudentView() {
287
 
288
  // Accordion Layout
289
  return `
290
- <div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl pt-24 sm:pt-4">
291
  <header class="flex justify-end items-center mb-6 sticky top-0 bg-slate-900/95 backdrop-blur z-20 py-4 px-2 -mx-2 border-b border-gray-800">
292
  <div class="flex flex-col items-end">
293
  <div class="flex items-center space-x-2">
@@ -305,12 +310,30 @@ export async function renderStudentView() {
305
  <div class="space-y-4">
306
  ${['beginner', 'intermediate', 'advanced'].map(level => {
307
  const tasks = levelGroups[level] || [];
308
- const isOpen = level === 'beginner' ? 'open' : '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  // Count completed
310
  const completedCount = tasks.filter(t => userProgress[t.id]?.status === 'completed').length;
 
311
 
312
  return `
313
- <details class="group border border-gray-700 rounded-xl bg-gray-800/30 overflow-hidden transition-all" ${isOpen}>
314
  <summary class="flex items-center justify-between p-4 cursor-pointer bg-gray-800/80 hover:bg-gray-700 transition-colors select-none">
315
  <div class="flex items-center space-x-3">
316
  <h3 class="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">
@@ -333,15 +356,15 @@ export async function renderStudentView() {
333
  }).join('')}
334
  </div>
335
 
336
- <!-- Peer Learning FAB -->
337
  <button onclick="window.openPeerModal()" class="fixed bottom-6 right-6 bg-purple-600 hover:bg-purple-500 text-white rounded-full p-4 shadow-xl shadow-purple-600/40 transition-transform hover:scale-110 active:scale-90 z-40"
338
  title="查看同學作業">
339
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
340
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0z" />
341
  </svg>
342
  </button>
343
- </div>
344
- `;
345
  }
346
 
347
  export function setupStudentEvents() {
@@ -371,8 +394,8 @@ export function setupStudentEvents() {
371
  };
372
 
373
  window.submitLevel = async (challengeId) => {
374
- const input = document.getElementById(`input-${challengeId}`);
375
- const errorMsg = document.getElementById(`error-${challengeId}`);
376
  const prompt = input.value;
377
  const roomCode = localStorage.getItem('vibecoding_room_code');
378
  const userId = localStorage.getItem('vibecoding_user_id');
@@ -418,7 +441,7 @@ export function setupStudentEvents() {
418
  const newCardHTML = renderTaskCard(challenge, newProgress);
419
 
420
  // 3. Replace in DOM
421
- const oldCard = document.getElementById(`card-${challengeId}`);
422
  if (oldCard) {
423
  oldCard.outerHTML = newCardHTML;
424
  } else {
@@ -485,12 +508,12 @@ function renderPeerModal() {
485
  let optionsHtml = '<option value="" disabled selected>選擇題目...</option>';
486
  if (cachedChallenges.length > 0) {
487
  optionsHtml += cachedChallenges.map(c =>
488
- `<option value="${c.id}">[${c.level}] ${c.title}</option>`
489
  ).join('');
490
  }
491
 
492
  return `
493
- <div id="peer-modal" class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm hidden flex items-center justify-center z-50 p-4">
494
  <div class="bg-gray-800 rounded-2xl w-full max-w-md h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
495
  <div class="p-6 border-b border-gray-700 flex justify-between items-center">
496
  <h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
@@ -507,7 +530,7 @@ function renderPeerModal() {
507
  <div class="text-center text-gray-500 mt-10">請選擇一個題目來查看</div>
508
  </div>
509
  </div>
510
- </div>
511
  `;
512
  }
513
 
@@ -543,7 +566,7 @@ window.loadPeerPrompts = async (challengeId) => {
543
  const isLiked = p.likedBy && p.likedBy.includes(currentUserId);
544
 
545
  return `
546
- <div class="bg-gray-700/30 p-4 rounded-xl border border-gray-600">
547
  <div class="flex items-center justify-between mb-2">
548
  <div class="flex items-center space-x-2">
549
  <div class="w-6 h-6 rounded-full bg-cyan-600 flex items-center justify-center text-xs font-bold text-white">
@@ -562,11 +585,11 @@ window.loadPeerPrompts = async (challengeId) => {
562
  </button>
563
  </div>
564
  <p class="text-gray-300 font-mono text-sm bg-black/20 p-3 rounded-lg border border-gray-700/50 whitespace-pre-wrap">${p.prompt}</p>
565
- </div>
566
  `}).join('');
567
 
568
  // Attach challenge title for notification context
569
- window.currentPeerChallengeTitle = document.querySelector(`#peer-challenge-select option[value="${challengeId}"]`).text;
570
  };
571
 
572
  // Like Handler
 
1
  import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserMonster, getUser, subscribeToUserProgress } from "../services/classroom.js";
2
+ import { generateMonsterSVG, getNextMonster, MONSTER_STAGES, MONSTER_DEFS } from "../utils/monsterUtils.js";
3
 
4
 
5
  // Cache challenges locally
 
198
  }
199
 
200
  return `
201
+ // Left sidebar position: Fixed Left-8 or similar.
202
+ // User wants it in the empty left area.
203
+ return `
204
+ < div id = "monster-container-fixed" class="fixed top-32 left-8 z-50 flex flex-col items-center group pointer-events-none sm:pointer-events-auto w-32 h-32" >
205
+ < !--Walking Container-- >
206
+ <div class="pixel-art-container relative transform transition-transform duration-500 ease-out origin-center"
207
  style="transform: scale(${currentScale}); animation: walk-float 6s ease-in-out infinite;">
208
 
209
  <div class="pixel-monster w-28 h-28 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out;">
 
216
  </div>
217
  </div>
218
 
219
+ <!--Evolution Prompt-- >
220
+ ${
221
+ canEvolve ? `
222
  <div id="evolution-prompt" class="absolute top-full mt-2 pointer-events-auto animate-bounce z-50">
223
  <div class="flex flex-col items-center">
224
  <div class="bg-gray-900/90 text-pink-200 text-xs py-2 px-3 rounded-xl border border-pink-500/30 shadow-lg text-center font-bold mb-1 backdrop-blur-sm whitespace-nowrap">
 
230
  </button>
231
  </div>
232
  </div>
233
+ ` : ''
234
+ }
235
 
236
+ < !--Stats Tooltip-- >
237
  <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-slate-300 p-3 rounded-xl border border-slate-700 mt-6 text-left pointer-events-auto shadow-2xl min-w-[120px]">
238
  <div class="font-bold text-white text-sm mb-1 text-center border-b border-gray-700 pb-1">${monster.name}</div>
239
  <div class="space-y-1 mt-1">
 
242
  <div class="flex justify-between"><span>⚔️ 任務:</span> <span class="text-yellow-400 font-bold">${totalCompleted}</span></div>
243
  </div>
244
  </div>
245
+ </div >
246
 
247
  <style>
248
  @keyframes breathe {
249
+ 0 %, 100 % { transform: translateY(0); filter: brightness(1); }
250
+ 50% {transform: translateY(-3px); filter: brightness(1.1); }
251
  }
252
  @keyframes walk-float {
253
+ 0 %, 100 % { transform: translateX(0) scale(${ currentScale }); }
254
+ 25% {transform: translateX(-10px) rotate(-2deg) scale(${currentScale}); }
255
+ 75% {transform: translateX(10px) rotate(2deg) scale(${currentScale}); }
256
  }
257
  </style>
258
+ `;
259
  };
260
 
261
  // Inject Initial Monster UI
 
292
 
293
  // Accordion Layout
294
  return `
295
+ < div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl pt-24 sm:pt-4" >
296
  <header class="flex justify-end items-center mb-6 sticky top-0 bg-slate-900/95 backdrop-blur z-20 py-4 px-2 -mx-2 border-b border-gray-800">
297
  <div class="flex flex-col items-end">
298
  <div class="flex items-center space-x-2">
 
310
  <div class="space-y-4">
311
  ${['beginner', 'intermediate', 'advanced'].map(level => {
312
  const tasks = levelGroups[level] || [];
313
+
314
+ // State Preservation Logic:
315
+ // Check DOM for existing details element and its open attribute
316
+ // ID strategy: details-{level}
317
+ // If not found, default to open for 'beginner', closed for others.
318
+ // But if we are re-rendering, we want to keep current state.
319
+
320
+ const detailsId = `details-group-${level}`;
321
+ const existingDetails = document.getElementById(detailsId);
322
+ let isOpenStr = '';
323
+
324
+ if (existingDetails) {
325
+ if (existingDetails.hasAttribute('open')) isOpenStr = 'open';
326
+ } else {
327
+ // Initial Load defaults
328
+ if (level === 'beginner') isOpenStr = 'open';
329
+ }
330
+
331
  // Count completed
332
  const completedCount = tasks.filter(t => userProgress[t.id]?.status === 'completed').length;
333
+ const allClear = completedCount === tasks.length && tasks.length > 0;
334
 
335
  return `
336
+ <details id="${detailsId}" class="group border border-gray-700 rounded-xl bg-gray-800/30 overflow-hidden transition-all" ${isOpenStr}>
337
  <summary class="flex items-center justify-between p-4 cursor-pointer bg-gray-800/80 hover:bg-gray-700 transition-colors select-none">
338
  <div class="flex items-center space-x-3">
339
  <h3 class="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">
 
356
  }).join('')}
357
  </div>
358
 
359
+ <!--Peer Learning FAB-- >
360
  <button onclick="window.openPeerModal()" class="fixed bottom-6 right-6 bg-purple-600 hover:bg-purple-500 text-white rounded-full p-4 shadow-xl shadow-purple-600/40 transition-transform hover:scale-110 active:scale-90 z-40"
361
  title="查看同學作業">
362
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
363
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0z" />
364
  </svg>
365
  </button>
366
+ </div >
367
+ `;
368
  }
369
 
370
  export function setupStudentEvents() {
 
394
  };
395
 
396
  window.submitLevel = async (challengeId) => {
397
+ const input = document.getElementById(`input - ${ challengeId } `);
398
+ const errorMsg = document.getElementById(`error - ${ challengeId } `);
399
  const prompt = input.value;
400
  const roomCode = localStorage.getItem('vibecoding_room_code');
401
  const userId = localStorage.getItem('vibecoding_user_id');
 
441
  const newCardHTML = renderTaskCard(challenge, newProgress);
442
 
443
  // 3. Replace in DOM
444
+ const oldCard = document.getElementById(`card - ${ challengeId } `);
445
  if (oldCard) {
446
  oldCard.outerHTML = newCardHTML;
447
  } else {
 
508
  let optionsHtml = '<option value="" disabled selected>選擇題目...</option>';
509
  if (cachedChallenges.length > 0) {
510
  optionsHtml += cachedChallenges.map(c =>
511
+ `< option value = "${c.id}" > [${ c.level }] ${ c.title }</option > `
512
  ).join('');
513
  }
514
 
515
  return `
516
+ < div id = "peer-modal" class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm hidden flex items-center justify-center z-50 p-4" >
517
  <div class="bg-gray-800 rounded-2xl w-full max-w-md h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
518
  <div class="p-6 border-b border-gray-700 flex justify-between items-center">
519
  <h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
 
530
  <div class="text-center text-gray-500 mt-10">請選擇一個題目來查看</div>
531
  </div>
532
  </div>
533
+ </div >
534
  `;
535
  }
536
 
 
566
  const isLiked = p.likedBy && p.likedBy.includes(currentUserId);
567
 
568
  return `
569
+ < div class="bg-gray-700/30 p-4 rounded-xl border border-gray-600" >
570
  <div class="flex items-center justify-between mb-2">
571
  <div class="flex items-center space-x-2">
572
  <div class="w-6 h-6 rounded-full bg-cyan-600 flex items-center justify-center text-xs font-bold text-white">
 
585
  </button>
586
  </div>
587
  <p class="text-gray-300 font-mono text-sm bg-black/20 p-3 rounded-lg border border-gray-700/50 whitespace-pre-wrap">${p.prompt}</p>
588
+ </div >
589
  `}).join('');
590
 
591
  // Attach challenge title for notification context
592
+ window.currentPeerChallengeTitle = document.querySelector(`#peer - challenge - select option[value = "${challengeId}"]`).text;
593
  };
594
 
595
  // Like Handler