Spaces:
Running
Running
Upload 8 files
Browse files- 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
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 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 |
-
<!--
|
| 179 |
-
<div class="absolute -bottom-2 -right-2 bg-gray-900/
|
| 180 |
-
Lv.${
|
| 181 |
</div>
|
| 182 |
</div>
|
| 183 |
|
| 184 |
-
<!-- Evolution
|
| 185 |
-
|
| 186 |
-
<div class="
|
| 187 |
-
<
|
| 188 |
-
<
|
| 189 |
-
<
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
</div>
|
| 194 |
</div>
|
| 195 |
` : ''}
|
| 196 |
|
| 197 |
-
<!-- Stats Tooltip
|
| 198 |
-
<div class="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-
|
| 199 |
-
<div class="font-bold text-white mb-1">${monster.name}</div>
|
| 200 |
-
<div
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
| 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">
|