Spaces:
Running
Running
Upload 8 files
Browse files- 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
const instructorSection = document.createElement('div');
|
| 198 |
-
instructorSection.className = 'flex flex-col items-center justify-center
|
| 199 |
instructorSection.innerHTML = `
|
| 200 |
-
<div class="relative
|
| 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-
|
| 204 |
|
| 205 |
-
<!-- Name Tag -->
|
| 206 |
-
<div class="absolute -bottom-
|
| 207 |
<span class="text-xl">👑</span>
|
| 208 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
</div>
|
| 210 |
</div>
|
| 211 |
`;
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
//
|
| 215 |
-
|
| 216 |
-
const
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
const progressMap = s.progress || {};
|
| 226 |
const totalLikes = Object.values(progressMap).reduce((acc, p) => acc + (p.likes || 0), 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
//
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
const card = document.createElement('div');
|
| 232 |
-
card.className = 'flex flex-col items-center group
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
card.innerHTML = `
|
| 234 |
-
<div class="relative w-24 h-24 flex items-center justify-center transform group-hover:-
|
| 235 |
-
<div class="w-
|
| 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-
|
| 246 |
-
<div class="text-
|
| 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 |
-
|
| 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');
|