Lashtw commited on
Commit
2eb9b8e
·
verified ·
1 Parent(s): 27174de

Upload 8 files

Browse files
Files changed (1) hide show
  1. src/views/StudentView.js +113 -64
src/views/StudentView.js CHANGED
@@ -1,4 +1,4 @@
1
- import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserStage, getUser } from "../services/classroom.js";
2
  import { generateMonsterSVG, getNextMonster, MONSTER_STAGES } from "../utils/monsterUtils.js";
3
 
4
 
@@ -130,90 +130,139 @@ export async function renderStudentView() {
130
  advanced: "高級 (Advanced)"
131
  };
132
 
133
- // --- Monster Evolution Logic ---
134
- let classSize = 1;
135
- let userProfile = {};
136
- try {
137
- classSize = await getClassSize(roomCode);
138
- userProfile = await getUser(userId) || {};
139
- } catch (e) { console.error("Fetch stats error", e); }
140
-
141
- // Calculate Stats
142
- const totalLikes = Object.values(userProgress).reduce((acc, p) => acc + (p.likes || 0), 0);
143
- const completedCounts = {
144
- 0: 0,
145
- 1: cachedChallenges.filter(c => c.level === 'beginner' && userProgress[c.id]?.status === 'completed').length,
146
- 2: cachedChallenges.filter(c => c.level === 'intermediate' && userProgress[c.id]?.status === 'completed').length,
147
- 3: cachedChallenges.filter(c => c.level === 'advanced' && userProgress[c.id]?.status === 'completed').length
148
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
- // 1. Calculate Potential Stage (What they qualify for)
151
- let potentialStage = 0;
152
- if (completedCounts[1] >= 5) potentialStage = 1;
153
- if (completedCounts[2] >= 5 && potentialStage >= 1) potentialStage = 2;
154
- if (completedCounts[3] >= 5 && potentialStage >= 2) potentialStage = 3;
155
-
156
- // 2. Get Actual Stage (What they have chosen/evolved to)
157
- const actualStage = userProfile.monster_stage || 0;
158
-
159
- // 3. Determine Display Logic
160
- // If Potential > Actual => Evolution Available.
161
- // If they don't evolve, they get BIGGER.
162
- // Scale Factor: 1.0 + (Potential - Actual) * 0.3
163
- const scale = 1 + (potentialStage - actualStage) * 0.3;
164
- const canEvolve = potentialStage > actualStage;
165
-
166
- // Get Monster Data for Current Actual Stage
167
- const monster = getNextMonster(actualStage, totalLikes, classSize);
168
-
169
- // Monster UI HTML
170
- const monsterHtml = `
171
- <div class="fixed top-6 left-6 z-50 flex flex-col items-center group pointer-events-none sm:pointer-events-auto">
172
- <!-- Monster with Dynamic Scale -->
173
- <div class="pixel-art-container relative transform transition-transform duration-500 ease-out hover:scale-110 origin-center" style="transform: scale(${scale});">
174
- <div class="pixel-monster w-20 h-20 sm:w-24 sm:h-24 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out;">
175
  ${generateMonsterSVG(monster)}
176
  </div>
177
 
178
- <!-- Stage Indicator Badge -->
179
- <div class="absolute -bottom-2 -right-2 bg-gray-900/80 text-xs text-yellow-500 px-1.5 py-0.5 rounded border border-yellow-500/30 font-mono transform scale-75 origin-top-left">
180
- Lv.${monster.stage}
181
  </div>
182
  </div>
183
 
184
- <!-- Evolution Trigger (Only if canEvolve) -->
185
- ${canEvolve ? `
186
- <div class="animate-bounce mt-4 pointer-events-auto">
187
- <button onclick="window.triggerEvolution(${actualStage + 1})" class="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-400 hover:to-purple-500 text-white font-bold py-2 px-4 rounded-full shadow-[0_0_15px_rgba(236,72,153,0.5)] border-2 border-white/20 text-xs sm:text-sm flex items-center space-x-2 transition-all hover:scale-105 active:scale-95">
188
- <span class="text-lg">✨</span>
189
- <span>進化 (Evolve)!</span>
190
- </button>
191
- <div class="text-[10px] text-center text-pink-300 mt-1 text-shadow bg-black/50 rounded px-1">
192
- 忽略並保持巨大!
 
 
 
 
193
  </div>
194
  </div>
195
  ` : ''}
196
 
197
- <!-- Stats Tooltip (Show on Hover) -->
198
- <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-gray-300 p-2 rounded-lg border border-gray-700 mt-2 text-center pointer-events-auto shadow-xl ${canEvolve ? 'mt-1' : 'mt-4'}">
199
- <div class="font-bold text-white mb-1">${monster.name}</div>
200
- <div>💖 Likes: ${totalLikes}</div>
201
- <div class="text-[10px] text-gray-500">Class: ${classSize}</div>
202
- ${scale > 1 ? `<div class="text-green-400 font-bold mt-1">巨大化 x${scale.toFixed(1)}</div>` : ''}
 
 
203
  </div>
204
  </div>
205
 
206
  <style>
207
  @keyframes breathe {
208
- 0%, 100% { transform: translateY(0); }
209
- 50% { transform: translateY(-3px); }
210
  }
211
  </style>
212
- `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
  // Accordion Layout
215
  return `
216
- ${monsterHtml}
217
  <div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl pt-24 sm:pt-4">
218
  <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">
219
  <div class="flex flex-col items-end">
 
1
+ import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserStage, getUser, subscribeToUserProgress } from "../services/classroom.js";
2
  import { generateMonsterSVG, getNextMonster, MONSTER_STAGES } from "../utils/monsterUtils.js";
3
 
4
 
 
130
  advanced: "高級 (Advanced)"
131
  };
132
 
133
+ // --- Monster Section Render Function ---
134
+ const renderMonsterSection = (currentUserProgress, classSize, userProfile) => {
135
+ // Calculate Stats
136
+ const totalLikes = Object.values(currentUserProgress).reduce((acc, p) => acc + (p.likes || 0), 0);
137
+
138
+ // Count completions per level
139
+ const counts = {
140
+ 1: cachedChallenges.filter(c => c.level === 'beginner' && currentUserProgress[c.id]?.status === 'completed').length,
141
+ 2: cachedChallenges.filter(c => c.level === 'intermediate' && currentUserProgress[c.id]?.status === 'completed').length,
142
+ 3: cachedChallenges.filter(c => c.level === 'advanced' && currentUserProgress[c.id]?.status === 'completed').length
143
+ };
144
+ const totalCompleted = counts[1] + counts[2] + counts[3];
145
+
146
+ // 1. Calculate Potential Stage
147
+ let potentialStage = 0;
148
+ if (counts[1] >= 5) potentialStage = 1;
149
+ if (counts[2] >= 5 && potentialStage >= 1) potentialStage = 2;
150
+ if (counts[3] >= 5 && potentialStage >= 2) potentialStage = 3;
151
+
152
+ // 2. Get Actual Stage
153
+ const actualStage = userProfile.monster_stage || 0;
154
+
155
+ // 3. Display Logic
156
+ const canEvolve = potentialStage > actualStage;
157
+
158
+ // Growth: Base Scale 1.0.
159
+ // Grows with TOTAL completed tasks. e.g. 0.05 per task.
160
+ // Also resets effective growth if we evolve?
161
+ // User said: "Monster size should grow every time a question is answered correctly"
162
+ // And "Level also rise".
163
+
164
+ // Let's make base scale depend on tasks completed SINCE last evolution?
165
+ // Or just total tasks.
166
+ // If I evolve, I probably want to start small-ish again?
167
+ // But "Stage 2" monster should probably be bigger than "Stage 1 Egg".
168
+ // Let's use a simple global scalar:
169
+ const growthFactor = 0.08;
170
+ const baseScale = 1.0;
171
+ // Adjust for stage so high stage monsters aren't tiny initially?
172
+ // Actually, let's just make it grow linearly based on total questions.
173
+ // But if I evolve, does it shrink?
174
+ // User request: "If don't evolve... keep getting bigger"
175
+ // Implicitly, evolving might reset the 'extra' growth or change the base form.
176
+ // Let's just use Total Completed for scale.
177
+ const currentScale = baseScale + (totalCompleted * growthFactor);
178
+
179
+ const monster = getNextMonster(actualStage, totalLikes, classSize);
180
 
181
+ return `
182
+ <div class="fixed top-8 left-10 z-50 flex flex-col items-center group pointer-events-none sm:pointer-events-auto">
183
+ <!-- Monster -->
184
+ <div class="pixel-art-container relative transform transition-transform duration-500 ease-out origin-center hover:scale-110" style="transform: scale(${currentScale});">
185
+ <div class="pixel-monster w-28 h-28 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out;">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  ${generateMonsterSVG(monster)}
187
  </div>
188
 
189
+ <!-- Level Indicator (Total Quests) -->
190
+ <div class="absolute -bottom-2 -right-2 bg-gray-900/90 text-xs text-yellow-400 px-2 py-0.5 rounded-full border border-yellow-500/50 font-mono font-bold transform scale-75 origin-top-left whitespace-nowrap">
191
+ Lv.${1 + totalCompleted}
192
  </div>
193
  </div>
194
 
195
+ <!-- Evolution Prompt -->
196
+ ${canEvolve ? `
197
+ <div class="absolute top-full mt-4 left-1/2 -translate-x-1/2 w-48 pointer-events-auto animate-bounce">
198
+ <div class="bg-gradient-to-br from-indigo-900 to-purple-900 border-2 border-pink-500 rounded-xl p-3 shadow-[0_0_20px_rgba(236,72,153,0.6)] text-center relative">
199
+ <!-- Triangle tip -->
200
+ <div class="absolute -top-2 left-1/2 -translate-x-1/2 w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-b-[8px] border-b-pink-500"></div>
201
+
202
+ <p class="text-xs text-pink-200 mb-2 font-bold leading-tight">
203
+ 咦,小怪獸的樣子正在發生變化...<br>否要進
204
+ </p>
205
+ <button onclick="window.triggerEvolution(${actualStage + 1})" class="w-full bg-pink-600 hover:bg-pink-500 text-white text-xs font-bold py-1.5 rounded-lg transition-colors shadow-sm">
206
+ ✨ 立即進化
207
+ </button>
208
  </div>
209
  </div>
210
  ` : ''}
211
 
212
+ <!-- Stats Tooltip -->
213
+ <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]">
214
+ <div class="font-bold text-white text-sm mb-1 text-center border-b border-gray-700 pb-1">${monster.name}</div>
215
+ <div class="space-y-1 mt-1">
216
+ <div class="flex justify-between"><span>💖 愛心:</span> <span class="text-pink-400 font-bold">${totalLikes}</span></div>
217
+ <div class="flex justify-between"><span>🏫 人數:</span> <span class="text-cyan-400 font-bold">${classSize}</span></div>
218
+ <div class="flex justify-between"><span>⚔️ 任務:</span> <span class="text-yellow-400 font-bold">${totalCompleted}</span></div>
219
+ </div>
220
  </div>
221
  </div>
222
 
223
  <style>
224
  @keyframes breathe {
225
+ 0%, 100% { transform: translateY(0); filter: brightness(1); }
226
+ 50% { transform: translateY(-3px); filter: brightness(1.1); }
227
  }
228
  </style>
229
+ `;
230
+ };
231
+
232
+ // Inject Initial Monster UI
233
+ const monsterContainerId = 'monster-ui-layer';
234
+ let monsterContainer = document.getElementById(monsterContainerId);
235
+ if (!monsterContainer) {
236
+ monsterContainer = document.createElement('div');
237
+ monsterContainer.id = monsterContainerId;
238
+ document.body.appendChild(monsterContainer);
239
+ }
240
+
241
+ // Initial Render
242
+ let classSize = 1;
243
+ let userProfile = {};
244
+ try {
245
+ classSize = await getClassSize(roomCode);
246
+ userProfile = await getUser(userId) || {};
247
+ } catch (e) { console.error("Fetch stats error", e); }
248
+
249
+ monsterContainer.innerHTML = renderMonsterSection(userProgress, classSize, userProfile);
250
+
251
+ // Setup Real-time Subscription
252
+ if (window.currentProgressUnsub) window.currentProgressUnsub();
253
+ window.currentProgressUnsub = subscribeToUserProgress(userId, (newProgressMap) => {
254
+ // Merge updates
255
+ const updatedProgress = { ...userProgress, ...newProgressMap };
256
+ // Re-render only Monster Section
257
+ monsterContainer.innerHTML = renderMonsterSection(updatedProgress, classSize, userProfile);
258
+
259
+ // Update userProgress ref for other logic?
260
+ // Note: 'userProgress' variable in this scope won't update for 'renderTaskCard' unless we reload.
261
+ // But for Monster UI it's fine.
262
+ });
263
 
264
  // Accordion Layout
265
  return `
 
266
  <div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl pt-24 sm:pt-4">
267
  <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">
268
  <div class="flex flex-col items-end">