Lashtw commited on
Commit
1323a0f
·
verified ·
1 Parent(s): 7d9742d

Upload 8 files

Browse files
Files changed (1) hide show
  1. src/views/StudentView.js +59 -60
src/views/StudentView.js CHANGED
@@ -369,50 +369,7 @@ export async function renderStudentView() {
369
 
370
  <div class="space-y-4">
371
  ${['beginner', 'intermediate', 'advanced'].map(level => {
372
- const tasks = levelGroups[level] || [];
373
-
374
- // State Preservation Logic:
375
- // Check DOM for existing details element and its open attribute
376
- // ID strategy: details-{level}
377
- // If not found, default to open for 'beginner', closed for others.
378
- // But if we are re-rendering, we want to keep current state.
379
-
380
- const detailsId = `details-group-${level}`;
381
- const existingDetails = document.getElementById(detailsId);
382
- let isOpenStr = '';
383
-
384
- if (existingDetails) {
385
- if (existingDetails.hasAttribute('open')) isOpenStr = 'open';
386
- } else {
387
- // Initial Load defaults
388
- if (level === 'beginner') isOpenStr = 'open';
389
- }
390
-
391
- // Count completed
392
- const completedCount = tasks.filter(t => userProgress[t.id]?.status === 'completed').length;
393
- const allClear = completedCount === tasks.length && tasks.length > 0;
394
-
395
- return `
396
- <details id="${detailsId}" class="group border border-gray-700 rounded-xl bg-gray-800/30 overflow-hidden transition-all" ${isOpenStr}>
397
- <summary class="flex items-center justify-between p-4 cursor-pointer bg-gray-800/80 hover:bg-gray-700 transition-colors select-none">
398
- <div class="flex items-center space-x-3">
399
- <h3 class="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">
400
- ${levelNames[level]}
401
- </h3>
402
- ${completedCount === tasks.length && tasks.length > 0 ? '<span class="text-yellow-500 text-xs border border-yellow-500/50 px-2 py-0.5 rounded-full">ALL CLEAR</span>' : ''}
403
- </div>
404
- <div class="flex items-center space-x-2">
405
- <span class="text-xs text-gray-400 bg-gray-900 px-2 py-1 rounded-full">${completedCount} / ${tasks.length}</span>
406
- <svg class="w-5 h-5 text-gray-400 transform group-open:rotate-180 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
407
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
408
- </svg>
409
- </div>
410
- </summary>
411
- <div class="p-4 pt-0 grid grid-cols-1 gap-4 mt-4">
412
- ${tasks.length > 0 ? tasks.map(c => renderTaskCard(c, userProgress)).join('') : '<div class="text-gray-500 text-sm italic">本區段尚無題目</div>'}
413
- </div>
414
- </details>
415
- `;
416
  }).join('')}
417
  </div>
418
 
@@ -491,28 +448,31 @@ export function setupStudentEvents() {
491
  btn.textContent = "✓ 已通關";
492
  btn.classList.add("bg-green-600");
493
 
494
- // NEW: Partial Update Strategy directly
495
- // 1. Find the container
496
- // The card is the great-great-grandparent of the button (button -> div -> div -> div -> card)
497
- // Or simpler: give ID to card wrapper in renderTaskCard.
498
- // Let's assume renderTaskCard now adds id="card-${c.id}"
499
 
500
- // 2. Re-render just this card
501
  const challenge = cachedChallenges.find(c => c.id === challengeId);
502
- const newProgress = { [challengeId]: { status: 'completed', submission_prompt: prompt } };
503
 
504
- // We need to merge with existing progress to pass to renderTaskCard?
505
- // Actually renderTaskCard takes the whole userProgress map.
506
- // But for this specific card, we can just pass a map containing just this one, because renderTaskCard(c, map) only looks up map[c.id].
 
 
 
 
 
 
 
507
 
508
- const newCardHTML = renderTaskCard(challenge, newProgress);
 
509
 
510
- // 3. Replace in DOM
511
- const oldCard = document.getElementById(`card-${challengeId}`);
512
- if (oldCard) {
513
- oldCard.outerHTML = newCardHTML;
514
  } else {
515
- // Fallback if ID not found (should not happen if we update renderTaskCard)
516
  const app = document.querySelector('#app');
517
  app.innerHTML = await renderStudentView();
518
  }
@@ -624,6 +584,45 @@ window.closePeerModal = () => {
624
  document.getElementById('peer-modal').classList.add('hidden');
625
  };
626
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  window.loadPeerPrompts = async (challengeId) => {
628
  const container = document.getElementById('peer-prompts-container');
629
  container.innerHTML = '<div class="text-center text-gray-400 py-10">載入中...</div>';
 
369
 
370
  <div class="space-y-4">
371
  ${['beginner', 'intermediate', 'advanced'].map(level => {
372
+ return renderLevelGroup(level, levelGroups[level] || [], userProgress, levelNames);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  }).join('')}
374
  </div>
375
 
 
448
  btn.textContent = "✓ 已通關";
449
  btn.classList.add("bg-green-600");
450
 
451
+ // Fetch latest progress to ensure correct count
452
+ const { getUserProgress } = await import("../services/classroom.js");
453
+ const newProgress = await getUserProgress(userId);
 
 
454
 
455
+ // Re-render the level group to update count and all-clear status
456
  const challenge = cachedChallenges.find(c => c.id === challengeId);
457
+ const level = challenge.level;
458
 
459
+ const levelGroups = {
460
+ beginner: cachedChallenges.filter(c => c.level === 'beginner'),
461
+ intermediate: cachedChallenges.filter(c => c.level === 'intermediate'),
462
+ advanced: cachedChallenges.filter(c => c.level === 'advanced')
463
+ };
464
+ const levelNames = {
465
+ beginner: "初級 (Beginner)",
466
+ intermediate: "中級 (Intermediate)",
467
+ advanced: "高級 (Advanced)"
468
+ };
469
 
470
+ const newGroupHTML = renderLevelGroup(level, levelGroups[level], newProgress, levelNames);
471
+ const detailEl = document.getElementById(`details-group-${level}`);
472
 
473
+ if (detailEl) {
474
+ detailEl.outerHTML = newGroupHTML;
 
 
475
  } else {
 
476
  const app = document.querySelector('#app');
477
  app.innerHTML = await renderStudentView();
478
  }
 
584
  document.getElementById('peer-modal').classList.add('hidden');
585
  };
586
 
587
+ // Helper to render a level group (Accordion)
588
+ function renderLevelGroup(level, tasks, userProgress, levelNames) {
589
+ const detailsId = `details-group-${level}`;
590
+ const existingDetails = document.getElementById(detailsId);
591
+ let isOpenStr = '';
592
+
593
+ if (existingDetails) {
594
+ if (existingDetails.hasAttribute('open')) isOpenStr = 'open';
595
+ } else {
596
+ // Initial Load defaults
597
+ if (level === 'beginner') isOpenStr = 'open';
598
+ }
599
+
600
+ // Count completed
601
+ const completedCount = tasks.filter(t => userProgress[t.id]?.status === 'completed').length;
602
+
603
+ return `
604
+ <details id="${detailsId}" class="group border border-gray-700 rounded-xl bg-gray-800/30 overflow-hidden transition-all" ${isOpenStr}>
605
+ <summary class="flex items-center justify-between p-4 cursor-pointer bg-gray-800/80 hover:bg-gray-700 transition-colors select-none">
606
+ <div class="flex items-center space-x-3">
607
+ <h3 class="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">
608
+ ${levelNames[level]}
609
+ </h3>
610
+ ${completedCount === tasks.length && tasks.length > 0 ? '<span class="text-yellow-500 text-xs border border-yellow-500/50 px-2 py-0.5 rounded-full">ALL CLEAR</span>' : ''}
611
+ </div>
612
+ <div class="flex items-center space-x-2">
613
+ <span class="text-xs text-gray-400 bg-gray-900 px-2 py-1 rounded-full">${completedCount} / ${tasks.length}</span>
614
+ <svg class="w-5 h-5 text-gray-400 transform group-open:rotate-180 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
615
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
616
+ </svg>
617
+ </div>
618
+ </summary>
619
+ <div class="p-4 pt-0 grid grid-cols-1 gap-4 mt-4">
620
+ ${tasks.length > 0 ? tasks.map(c => renderTaskCard(c, userProgress)).join('') : '<div class="text-gray-500 text-sm italic">本區段尚無題目</div>'}
621
+ </div>
622
+ </details>
623
+ `;
624
+ }
625
+
626
  window.loadPeerPrompts = async (challengeId) => {
627
  const container = document.getElementById('peer-prompts-container');
628
  container.innerHTML = '<div class="text-center text-gray-400 py-10">載入中...</div>';