Spaces:
Running
Running
Upload 8 files
Browse files- src/views/InstructorView.js +133 -7
src/views/InstructorView.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
|
|
|
|
| 2 |
import { generateMonsterSVG, getNextMonster } from "../utils/monsterUtils.js";
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
let cachedChallenges = [];
|
| 5 |
let currentStudents = [];
|
| 6 |
|
|
@@ -78,9 +84,21 @@ export async function renderInstructorView() {
|
|
| 78 |
<p class="text-gray-400 mt-2 font-mono" id="photo-date">2026.01.27</p>
|
| 79 |
</div>
|
| 80 |
|
| 81 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
<!-- Dynamic Content -->
|
| 83 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
</div>
|
| 85 |
|
| 86 |
<div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
|
|
@@ -180,6 +198,102 @@ export function setupInstructorEvents() {
|
|
| 180 |
const displayRoomCode = document.getElementById('display-room-code');
|
| 181 |
const navAdminBtn = document.getElementById('nav-admin-btn');
|
| 182 |
const groupPhotoBtn = document.getElementById('group-photo-btn');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
// Group Photo Logic
|
| 185 |
groupPhotoBtn.addEventListener('click', () => {
|
|
@@ -257,17 +371,29 @@ export function setupInstructorEvents() {
|
|
| 257 |
let finalAngle = baseAngle + angleJitter;
|
| 258 |
|
| 259 |
// Collision Avoidance for Bottom Label (approx 90 deg / PI/2)
|
| 260 |
-
//
|
| 261 |
const deg = finalAngle * (180 / Math.PI) % 360;
|
| 262 |
const normalizedDeg = deg < 0 ? deg + 360 : deg;
|
| 263 |
|
| 264 |
-
// Avoid "South" (approx
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
}
|
| 268 |
|
| 269 |
-
// Radius: Random within range
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
const xOff = Math.cos(finalAngle) * radius;
|
| 273 |
const yOff = Math.sin(finalAngle) * radius * 0.8;
|
|
|
|
| 1 |
import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
|
| 2 |
+
import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
|
| 3 |
import { generateMonsterSVG, getNextMonster } from "../utils/monsterUtils.js";
|
| 4 |
|
| 5 |
+
// Load html2canvas dynamically
|
| 6 |
+
const script = document.createElement('script');
|
| 7 |
+
script.src = "https://html2canvas.hertzen.com/dist/html2canvas.min.js";
|
| 8 |
+
document.head.appendChild(script);
|
| 9 |
+
|
| 10 |
let cachedChallenges = [];
|
| 11 |
let currentStudents = [];
|
| 12 |
|
|
|
|
| 84 |
<p class="text-gray-400 mt-2 font-mono" id="photo-date">2026.01.27</p>
|
| 85 |
</div>
|
| 86 |
|
| 87 |
+
<div class="absolute top-6 left-6 z-50 flex space-x-4">
|
| 88 |
+
<button id="snapshot-btn" class="bg-white/10 hover:bg-white/20 text-white border border-white/30 font-bold py-2 px-6 rounded-full backdrop-blur-md transition-all flex items-center space-x-2 shadow-lg group">
|
| 89 |
+
<span class="text-2xl group-hover:scale-110 transition-transform">📸</span>
|
| 90 |
+
<span>拍照 (Snapshot)</span>
|
| 91 |
+
</button>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div id="group-photo-container" class="w-full max-w-7xl flex flex-col items-center overflow-y-auto max-h-[80vh] custom-scrollbar relative">
|
| 95 |
<!-- Dynamic Content -->
|
| 96 |
</div>
|
| 97 |
+
|
| 98 |
+
<!-- Countdown Overlay -->
|
| 99 |
+
<div id="snapshot-overlay" class="absolute inset-0 z-[60] hidden flex-col items-center justify-center pointer-events-none">
|
| 100 |
+
<div id="countdown-number" class="text-[150px] font-black text-white drop-shadow-[0_0_50px_rgba(0,0,0,0.8)] animate-pulse">3</div>
|
| 101 |
+
</div>
|
| 102 |
</div>
|
| 103 |
|
| 104 |
<div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
|
|
|
|
| 198 |
const displayRoomCode = document.getElementById('display-room-code');
|
| 199 |
const navAdminBtn = document.getElementById('nav-admin-btn');
|
| 200 |
const groupPhotoBtn = document.getElementById('group-photo-btn');
|
| 201 |
+
const snapshotBtn = document.getElementById('snapshot-btn');
|
| 202 |
+
let isSnapshotting = false;
|
| 203 |
+
|
| 204 |
+
// Snapshot Logic
|
| 205 |
+
snapshotBtn.addEventListener('click', async () => {
|
| 206 |
+
if (isSnapshotting || typeof html2canvas === 'undefined') return;
|
| 207 |
+
isSnapshotting = true;
|
| 208 |
+
|
| 209 |
+
const overlay = document.getElementById('snapshot-overlay');
|
| 210 |
+
const countEl = document.getElementById('countdown-number');
|
| 211 |
+
const container = document.getElementById('group-photo-container');
|
| 212 |
+
const modal = document.getElementById('group-photo-modal');
|
| 213 |
+
|
| 214 |
+
// Close button hide
|
| 215 |
+
const closeBtn = modal.querySelector('button');
|
| 216 |
+
if (closeBtn) closeBtn.style.opacity = '0';
|
| 217 |
+
snapshotBtn.style.opacity = '0';
|
| 218 |
+
|
| 219 |
+
overlay.classList.remove('hidden');
|
| 220 |
+
overlay.classList.add('flex');
|
| 221 |
+
|
| 222 |
+
// Countdown Sequence
|
| 223 |
+
const runCountdown = (num) => new Promise(resolve => {
|
| 224 |
+
countEl.textContent = num;
|
| 225 |
+
countEl.style.transform = 'scale(1.5)';
|
| 226 |
+
countEl.style.opacity = '1';
|
| 227 |
+
|
| 228 |
+
// Animation reset
|
| 229 |
+
requestAnimationFrame(() => {
|
| 230 |
+
countEl.style.transition = 'all 0.5s ease-out';
|
| 231 |
+
countEl.style.transform = 'scale(1)';
|
| 232 |
+
countEl.style.opacity = '0.5';
|
| 233 |
+
setTimeout(resolve, 1000);
|
| 234 |
+
});
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
await runCountdown(3);
|
| 238 |
+
await runCountdown(2);
|
| 239 |
+
await runCountdown(1);
|
| 240 |
+
|
| 241 |
+
// Action!
|
| 242 |
+
countEl.textContent = '';
|
| 243 |
+
overlay.classList.add('hidden');
|
| 244 |
+
|
| 245 |
+
// 1. Emojis Explosion
|
| 246 |
+
const emojis = ['🤘', '✌️', '👍', '🫶', '😎', '🔥'];
|
| 247 |
+
const cards = container.querySelectorAll('.group\\/card');
|
| 248 |
+
|
| 249 |
+
cards.forEach(card => {
|
| 250 |
+
// Random Emoji
|
| 251 |
+
const emoji = emojis[Math.floor(Math.random() * emojis.length)];
|
| 252 |
+
const emojiEl = document.createElement('div');
|
| 253 |
+
emojiEl.textContent = emoji;
|
| 254 |
+
emojiEl.className = 'absolute -top-12 left-1/2 transform -translate-x-1/2 text-4xl animate-bounce z-50 drop-shadow-lg';
|
| 255 |
+
emojiEl.style.animationDuration = '0.5s';
|
| 256 |
+
card.appendChild(emojiEl);
|
| 257 |
+
|
| 258 |
+
// Remove after 2s
|
| 259 |
+
setTimeout(() => emojiEl.remove(), 3000);
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
// 2. Capture
|
| 263 |
+
setTimeout(async () => {
|
| 264 |
+
try {
|
| 265 |
+
// Flash Effect
|
| 266 |
+
const flash = document.createElement('div');
|
| 267 |
+
flash.className = 'fixed inset-0 bg-white z-[100] transition-opacity duration-300 pointer-events-none';
|
| 268 |
+
document.body.appendChild(flash);
|
| 269 |
+
setTimeout(() => flash.style.opacity = '0', 50);
|
| 270 |
+
setTimeout(() => flash.remove(), 300);
|
| 271 |
+
|
| 272 |
+
const canvas = await html2canvas(container, {
|
| 273 |
+
backgroundColor: '#111827', // Match bg-gray-900
|
| 274 |
+
scale: 2, // High res
|
| 275 |
+
useCORS: true,
|
| 276 |
+
logging: false
|
| 277 |
+
});
|
| 278 |
+
|
| 279 |
+
// Download
|
| 280 |
+
const link = document.createElement('a');
|
| 281 |
+
const dateStr = new Date().toISOString().slice(0, 10);
|
| 282 |
+
link.download = `VIBE_Class_Photo_${dateStr}.png`;
|
| 283 |
+
link.href = canvas.toDataURL();
|
| 284 |
+
link.click();
|
| 285 |
+
|
| 286 |
+
} catch (e) {
|
| 287 |
+
console.error("Snapshot failed:", e);
|
| 288 |
+
alert("截圖失敗,請手動截圖 (Press PrtSc)");
|
| 289 |
+
} finally {
|
| 290 |
+
// Restore UI
|
| 291 |
+
if (closeBtn) closeBtn.style.opacity = '1';
|
| 292 |
+
snapshotBtn.style.opacity = '1';
|
| 293 |
+
isSnapshotting = false;
|
| 294 |
+
}
|
| 295 |
+
}, 600); // Slight delay for emojis to appear
|
| 296 |
+
});
|
| 297 |
|
| 298 |
// Group Photo Logic
|
| 299 |
groupPhotoBtn.addEventListener('click', () => {
|
|
|
|
| 371 |
let finalAngle = baseAngle + angleJitter;
|
| 372 |
|
| 373 |
// Collision Avoidance for Bottom Label (approx 90 deg / PI/2)
|
| 374 |
+
// Expanded exclusion zone for better label clearance (60 to 120 degrees)
|
| 375 |
const deg = finalAngle * (180 / Math.PI) % 360;
|
| 376 |
const normalizedDeg = deg < 0 ? deg + 360 : deg;
|
| 377 |
|
| 378 |
+
// Avoid "South" (approx 60 to 120 degrees) where the label sticks out
|
| 379 |
+
// Also push further away if near the bottom
|
| 380 |
+
if (normalizedDeg > 60 && normalizedDeg < 120) {
|
| 381 |
+
// Push angle strictly out of the zone
|
| 382 |
+
const distTo60 = Math.abs(normalizedDeg - 60);
|
| 383 |
+
const distTo120 = Math.abs(normalizedDeg - 120);
|
| 384 |
+
|
| 385 |
+
if (distTo60 < distTo120) {
|
| 386 |
+
finalAngle = (60 - 10) * (Math.PI / 180); // Move to 50 deg
|
| 387 |
+
} else {
|
| 388 |
+
finalAngle = (120 + 10) * (Math.PI / 180); // Move to 130 deg
|
| 389 |
+
}
|
| 390 |
}
|
| 391 |
|
| 392 |
+
// Radius: Random within range (Push out further if still somewhat south)
|
| 393 |
+
let radius = minR + Math.random() * (maxR - minR);
|
| 394 |
+
if (normalizedDeg > 45 && normalizedDeg < 135) {
|
| 395 |
+
radius += 60; // Extra buffer for bottom elements
|
| 396 |
+
}
|
| 397 |
|
| 398 |
const xOff = Math.cos(finalAngle) * radius;
|
| 399 |
const yOff = Math.sin(finalAngle) * radius * 0.8;
|