Spaces:
Running
Running
Upload 10 files
Browse files- .gitattributes +1 -0
- assets/instructor_avatar.png +3 -0
- src/views/InstructorView.js +103 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/instructor_avatar.png filter=lfs diff=lfs merge=lfs -text
|
assets/instructor_avatar.png
ADDED
|
|
Git LFS Details
|
src/views/InstructorView.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js";
|
|
|
|
| 2 |
|
| 3 |
let cachedChallenges = [];
|
|
|
|
| 4 |
|
| 5 |
export async function renderInstructorView() {
|
| 6 |
// Pre-fetch challenges for table headers
|
|
@@ -65,6 +67,22 @@ export async function renderInstructorView() {
|
|
| 65 |
</div>
|
| 66 |
</div>
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
<div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
|
| 69 |
<!-- Header -->
|
| 70 |
<header class="flex flex-col md:flex-row justify-between items-center mb-6 bg-gray-800 p-4 rounded-xl border border-gray-700 space-y-4 md:space-y-0 sticky top-0 z-30 shadow-lg">
|
|
@@ -86,6 +104,9 @@ export async function renderInstructorView() {
|
|
| 86 |
<div class="flex items-center"><div class="w-3 h-3 bg-red-500 animate-pulse rounded-sm mr-1"></div> 卡關 (>5m)</div>
|
| 87 |
</div>
|
| 88 |
|
|
|
|
|
|
|
|
|
|
| 89 |
<button id="nav-admin-btn" class="bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition-all border border-gray-600">
|
| 90 |
管理題目
|
| 91 |
</button>
|
|
@@ -158,6 +179,87 @@ export function setupInstructorEvents() {
|
|
| 158 |
const dashboardContent = document.getElementById('dashboard-content');
|
| 159 |
const displayRoomCode = document.getElementById('display-room-code');
|
| 160 |
const navAdminBtn = document.getElementById('nav-admin-btn');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
navAdminBtn.addEventListener('click', () => {
|
| 163 |
// Save current room to return later
|
|
@@ -201,6 +303,7 @@ export function setupInstructorEvents() {
|
|
| 201 |
|
| 202 |
// Subscribe to updates
|
| 203 |
subscribeToRoom(roomCode, (students) => {
|
|
|
|
| 204 |
renderTransposedHeatmap(students);
|
| 205 |
});
|
| 206 |
}
|
|
|
|
| 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 |
|
| 7 |
export async function renderInstructorView() {
|
| 8 |
// Pre-fetch challenges for table headers
|
|
|
|
| 67 |
</div>
|
| 68 |
</div>
|
| 69 |
|
| 70 |
+
<!-- Group Photo Modal -->
|
| 71 |
+
<div id="group-photo-modal" class="fixed inset-0 bg-gray-900/95 backdrop-blur-md z-50 hidden flex flex-col items-center justify-center p-4 transition-opacity duration-300">
|
| 72 |
+
<button onclick="document.getElementById('group-photo-modal').classList.add('hidden')" class="absolute top-6 right-6 text-gray-400 hover:text-white text-4xl z-50">✕</button>
|
| 73 |
+
|
| 74 |
+
<div class="text-center mb-8 z-10">
|
| 75 |
+
<h2 class="text-3xl md:text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 via-orange-500 to-red-500 tracking-wider drop-shadow-lg">
|
| 76 |
+
大合照 CLASS PHOTO
|
| 77 |
+
</h2>
|
| 78 |
+
<p class="text-gray-400 mt-2 font-mono" id="photo-date">2026.01.27</p>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div id="group-photo-container" class="w-full max-w-7xl flex flex-col items-center overflow-y-auto max-h-[80vh] custom-scrollbar">
|
| 82 |
+
<!-- Dynamic Content -->
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
<div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
|
| 87 |
<!-- Header -->
|
| 88 |
<header class="flex flex-col md:flex-row justify-between items-center mb-6 bg-gray-800 p-4 rounded-xl border border-gray-700 space-y-4 md:space-y-0 sticky top-0 z-30 shadow-lg">
|
|
|
|
| 104 |
<div class="flex items-center"><div class="w-3 h-3 bg-red-500 animate-pulse rounded-sm mr-1"></div> 卡關 (>5m)</div>
|
| 105 |
</div>
|
| 106 |
|
| 107 |
+
<button id="group-photo-btn" class="bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 text-white font-bold py-2 px-4 rounded-lg transition-all shadow-lg border border-pink-400/30 flex items-center space-x-2">
|
| 108 |
+
<span>📸 大合照</span>
|
| 109 |
+
</button>
|
| 110 |
<button id="nav-admin-btn" class="bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition-all border border-gray-600">
|
| 111 |
管理題目
|
| 112 |
</button>
|
|
|
|
| 179 |
const dashboardContent = document.getElementById('dashboard-content');
|
| 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', () => {
|
| 186 |
+
const modal = document.getElementById('group-photo-modal');
|
| 187 |
+
const container = document.getElementById('group-photo-container');
|
| 188 |
+
const dateEl = document.getElementById('photo-date');
|
| 189 |
+
|
| 190 |
+
// Update Date
|
| 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. Instructor Section (Center Top)
|
| 197 |
+
const instructorSection = document.createElement('div');
|
| 198 |
+
instructorSection.className = 'flex flex-col items-center justify-center mb-10 w-full relative';
|
| 199 |
+
instructorSection.innerHTML = `
|
| 200 |
+
<div class="relative group animate-bounce-slow">
|
| 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-40 h-40 md:w-56 md:h-56 object-contain pixel-art drop-shadow-[0_10px_20px_rgba(0,0,0,0.5)] z-10 hover:scale-105 transition-transform duration-300" alt="Instructor">
|
| 204 |
+
|
| 205 |
+
<!-- Name Tag -->
|
| 206 |
+
<div class="absolute -bottom-6 left-1/2 transform -translate-x-1/2 bg-black/80 backdrop-blur text-yellow-400 px-4 py-1.5 rounded-full border border-yellow-500/30 shadow-xl flex items-center space-x-2 z-20 whitespace-nowrap">
|
| 207 |
+
<span class="text-xl">👑</span>
|
| 208 |
+
<span class="font-bold text-lg">講師 (Instructor)</span>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
`;
|
| 212 |
+
container.appendChild(instructorSection);
|
| 213 |
+
|
| 214 |
+
// 2. Students Grid
|
| 215 |
+
if (currentStudents.length === 0) {
|
| 216 |
+
const emptyMsg = document.createElement('div');
|
| 217 |
+
emptyMsg.className = 'text-gray-500 text-xl mt-10';
|
| 218 |
+
emptyMsg.textContent = '尚無學員加入...';
|
| 219 |
+
container.appendChild(emptyMsg);
|
| 220 |
+
} else {
|
| 221 |
+
const grid = document.createElement('div');
|
| 222 |
+
grid.className = 'grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-6 md:gap-8 w-full px-4 md:px-10';
|
| 223 |
+
|
| 224 |
+
currentStudents.forEach(s => {
|
| 225 |
+
const progressMap = s.progress || {};
|
| 226 |
+
const totalLikes = Object.values(progressMap).reduce((acc, p) => acc + (p.likes || 0), 0);
|
| 227 |
+
|
| 228 |
+
// Get Monster
|
| 229 |
+
const monster = getNextMonster(s.monster_stage || 0, totalLikes, currentStudents.length, s.monster_id);
|
| 230 |
+
|
| 231 |
+
const card = document.createElement('div');
|
| 232 |
+
card.className = 'flex flex-col items-center group relative';
|
| 233 |
+
card.innerHTML = `
|
| 234 |
+
<div class="relative w-24 h-24 flex items-center justify-center transform group-hover:-translate-y-2 transition-transform duration-300">
|
| 235 |
+
<div class="w-20 h-20 md:w-24 md:h-24 pixel-art drop-shadow-lg filter group-hover:brightness-110 transition-all">
|
| 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-2 text-center bg-gray-800/60 backdrop-blur rounded-lg px-3 py-1.5 border border-gray-700 group-hover:border-cyan-500/50 transition-colors w-full max-w-[120px]">
|
| 246 |
+
<div class="text-sm font-bold text-cyan-200 truncate">${s.nickname}</div>
|
| 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 |
+
grid.appendChild(card);
|
| 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
|
|
|
|
| 303 |
|
| 304 |
// Subscribe to updates
|
| 305 |
subscribeToRoom(roomCode, (students) => {
|
| 306 |
+
currentStudents = students;
|
| 307 |
renderTransposedHeatmap(students);
|
| 308 |
});
|
| 309 |
}
|