Spaces:
Running
Running
Upload 9 files
Browse files- src/services/classroom.js +11 -2
- src/views/InstructorView.js +41 -12
src/services/classroom.js
CHANGED
|
@@ -11,9 +11,9 @@ import {
|
|
| 11 |
where,
|
| 12 |
getDocs,
|
| 13 |
orderBy,
|
| 14 |
-
deleteDoc,
|
| 15 |
updateDoc,
|
| 16 |
-
getCountFromServer
|
|
|
|
| 17 |
} from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
|
| 18 |
|
| 19 |
// Collection references
|
|
@@ -509,3 +509,12 @@ export function subscribeToUserProgress(userId, callback) {
|
|
| 509 |
callback(progressMap);
|
| 510 |
});
|
| 511 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
where,
|
| 12 |
getDocs,
|
| 13 |
orderBy,
|
|
|
|
| 14 |
updateDoc,
|
| 15 |
+
getCountFromServer,
|
| 16 |
+
deleteDoc
|
| 17 |
} from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
|
| 18 |
|
| 19 |
// Collection references
|
|
|
|
| 509 |
callback(progressMap);
|
| 510 |
});
|
| 511 |
}
|
| 512 |
+
|
| 513 |
+
/**
|
| 514 |
+
* Removes a user from the classroom (Kick)
|
| 515 |
+
* @param {string} userId
|
| 516 |
+
*/
|
| 517 |
+
export async function removeUser(userId) {
|
| 518 |
+
if (!userId) return;
|
| 519 |
+
await deleteDoc(doc(db, USERS_COLLECTION, userId));
|
| 520 |
+
}
|
src/views/InstructorView.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
|
| 2 |
|
| 3 |
import { generateMonsterSVG, getNextMonster } from "../utils/monsterUtils.js";
|
| 4 |
|
|
@@ -378,6 +378,18 @@ export function setupInstructorEvents() {
|
|
| 378 |
const students = [...currentStudents].sort(() => Math.random() - 0.5);
|
| 379 |
const total = students.length;
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
students.forEach((s, index) => {
|
| 382 |
const progressMap = s.progress || {};
|
| 383 |
const totalLikes = Object.values(progressMap).reduce((acc, p) => acc + (p.likes || 0), 0);
|
|
@@ -398,24 +410,36 @@ export function setupInstructorEvents() {
|
|
| 398 |
const deg = finalAngle * (180 / Math.PI) % 360;
|
| 399 |
const normalizedDeg = deg < 0 ? deg + 360 : deg;
|
| 400 |
|
| 401 |
-
// Avoid
|
| 402 |
-
// Also push further away if near the bottom
|
| 403 |
if (normalizedDeg > 60 && normalizedDeg < 120) {
|
| 404 |
// Push angle strictly out of the zone
|
| 405 |
const distTo60 = Math.abs(normalizedDeg - 60);
|
| 406 |
const distTo120 = Math.abs(normalizedDeg - 120);
|
| 407 |
|
| 408 |
if (distTo60 < distTo120) {
|
| 409 |
-
finalAngle = (60 -
|
| 410 |
} else {
|
| 411 |
-
finalAngle = (120 +
|
| 412 |
}
|
| 413 |
}
|
| 414 |
|
| 415 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
let radius = minR + Math.random() * (maxR - minR);
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
}
|
| 420 |
|
| 421 |
const xOff = Math.cos(finalAngle) * radius;
|
|
@@ -444,7 +468,7 @@ export function setupInstructorEvents() {
|
|
| 444 |
</div>
|
| 445 |
|
| 446 |
<!-- Monster Image -->
|
| 447 |
-
<div class="monster-img-container relative
|
| 448 |
<div class="w-full h-full pixel-art drop-shadow-md filter group-hover/card:brightness-110 transition-all">
|
| 449 |
${generateMonsterSVG(monster)}
|
| 450 |
</div>
|
|
@@ -609,9 +633,14 @@ function renderTransposedHeatmap(students) {
|
|
| 609 |
<!-- Online Indicator (Simulated) -->
|
| 610 |
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-gray-800 rounded-full"></div>
|
| 611 |
</div>
|
| 612 |
-
<
|
| 613 |
-
|
| 614 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
</div>
|
| 616 |
</th>
|
| 617 |
`;
|
|
|
|
| 1 |
+
import { createRoom, subscribeToRoom, getChallenges, resetProgress, removeUser } from "../services/classroom.js";
|
| 2 |
|
| 3 |
import { generateMonsterSVG, getNextMonster } from "../utils/monsterUtils.js";
|
| 4 |
|
|
|
|
| 378 |
const students = [...currentStudents].sort(() => Math.random() - 0.5);
|
| 379 |
const total = students.length;
|
| 380 |
|
| 381 |
+
// --- Dynamic Sizing Logic ---
|
| 382 |
+
let sizeClass = 'w-20 h-20 md:w-24 md:h-24'; // Default (Size 100%)
|
| 383 |
+
let scaleFactor = 1.0;
|
| 384 |
+
|
| 385 |
+
if (total >= 40) {
|
| 386 |
+
sizeClass = 'w-12 h-12 md:w-14 md:h-14'; // Size 60%
|
| 387 |
+
scaleFactor = 0.6;
|
| 388 |
+
} else if (total >= 20) {
|
| 389 |
+
sizeClass = 'w-16 h-16 md:w-20 md:h-20'; // Size 80%
|
| 390 |
+
scaleFactor = 0.8;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
students.forEach((s, index) => {
|
| 394 |
const progressMap = s.progress || {};
|
| 395 |
const totalLikes = Object.values(progressMap).reduce((acc, p) => acc + (p.likes || 0), 0);
|
|
|
|
| 410 |
const deg = finalAngle * (180 / Math.PI) % 360;
|
| 411 |
const normalizedDeg = deg < 0 ? deg + 360 : deg;
|
| 412 |
|
| 413 |
+
// 1. Avoid Instructor Label (Bottom Center: 60-120 deg)
|
|
|
|
| 414 |
if (normalizedDeg > 60 && normalizedDeg < 120) {
|
| 415 |
// Push angle strictly out of the zone
|
| 416 |
const distTo60 = Math.abs(normalizedDeg - 60);
|
| 417 |
const distTo120 = Math.abs(normalizedDeg - 120);
|
| 418 |
|
| 419 |
if (distTo60 < distTo120) {
|
| 420 |
+
finalAngle = (60 - 15) * (Math.PI / 180); // Move to 45 deg
|
| 421 |
} else {
|
| 422 |
+
finalAngle = (120 + 15) * (Math.PI / 180); // Move to 135 deg
|
| 423 |
}
|
| 424 |
}
|
| 425 |
|
| 426 |
+
// 2. Recalculate degree after shift
|
| 427 |
+
const finalDeg = finalAngle * (180 / Math.PI) % 360;
|
| 428 |
+
const normFinalDeg = finalDeg < 0 ? finalDeg + 360 : finalDeg;
|
| 429 |
+
|
| 430 |
+
// Radius: Random within range
|
| 431 |
let radius = minR + Math.random() * (maxR - minR);
|
| 432 |
+
|
| 433 |
+
// 3. Avoid Watermark (Bottom Right: 0-60 deg)
|
| 434 |
+
// If in this sector, pull them in closer to center to avoid the corner watermark
|
| 435 |
+
if (normFinalDeg >= 0 && normFinalDeg < 60) {
|
| 436 |
+
radius = minR + Math.random() * 40; // Max radius restricted to minR + 40 (approx 260px)
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// 4. Extra space for bottom area (outside watermark/label zones)
|
| 440 |
+
// If 120-150 deg (Bottom Left), can go further out
|
| 441 |
+
if (normFinalDeg > 120 && normFinalDeg < 150) {
|
| 442 |
+
radius += 40;
|
| 443 |
}
|
| 444 |
|
| 445 |
const xOff = Math.cos(finalAngle) * radius;
|
|
|
|
| 468 |
</div>
|
| 469 |
|
| 470 |
<!-- Monster Image -->
|
| 471 |
+
<div class="monster-img-container relative ${sizeClass} 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;">
|
| 472 |
<div class="w-full h-full pixel-art drop-shadow-md filter group-hover/card:brightness-110 transition-all">
|
| 473 |
${generateMonsterSVG(monster)}
|
| 474 |
</div>
|
|
|
|
| 633 |
<!-- Online Indicator (Simulated) -->
|
| 634 |
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-gray-800 rounded-full"></div>
|
| 635 |
</div>
|
| 636 |
+
<div class="flex items-center justify-center space-x-1">
|
| 637 |
+
<span class="text-xs text-gray-300 font-medium truncate max-w-[80px] writing-vertical-lr" style="writing-mode: vertical-rl; text-orientation: mixed;">
|
| 638 |
+
${student.nickname}
|
| 639 |
+
</span>
|
| 640 |
+
<button onclick="window.confirmKick('${student.id}', '${student.nickname}')" class="text-gray-600 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity" title="踢出學員">
|
| 641 |
+
🗑️
|
| 642 |
+
</button>
|
| 643 |
+
</div>
|
| 644 |
</div>
|
| 645 |
</th>
|
| 646 |
`;
|