Lashtw commited on
Commit
051bc5c
·
verified ·
1 Parent(s): b391d41

Upload 8 files

Browse files
src/services/classroom.js CHANGED
@@ -37,6 +37,24 @@ export async function createRoom() {
37
  return roomCode;
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  /**
41
  * Joins a room with Dual-Role Auth / Session Persistence logic
42
  * @param {string} roomCode
 
37
  return roomCode;
38
  }
39
 
40
+ /**
41
+ * Verifies instructor password against Firestore (Auto-seeds if missing)
42
+ * @param {string} inputPassword
43
+ * @returns {Promise<boolean>}
44
+ */
45
+ export async function verifyInstructorPassword(inputPassword) {
46
+ const settingsRef = doc(db, "settings", "instructor_auth");
47
+ const snap = await getDoc(settingsRef);
48
+
49
+ if (!snap.exists()) {
50
+ // Auto-seed default password for migration
51
+ await setDoc(settingsRef, { password: "88300" });
52
+ return inputPassword === "88300";
53
+ }
54
+
55
+ return snap.data().password === inputPassword;
56
+ }
57
+
58
  /**
59
  * Joins a room with Dual-Role Auth / Session Persistence logic
60
  * @param {string} roomCode
src/views/InstructorView.js CHANGED
@@ -120,12 +120,27 @@ export function setupInstructorEvents() {
120
  const authModal = document.getElementById('auth-modal');
121
 
122
  // Default password check
123
- const checkPassword = () => {
124
- if (pwdInput.value === '88300') {
125
- authModal.classList.add('hidden');
126
- } else {
127
- alert('密碼錯誤');
128
- pwdInput.value = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
130
  };
131
 
 
120
  const authModal = document.getElementById('auth-modal');
121
 
122
  // Default password check
123
+ const checkPassword = async () => {
124
+ const { verifyInstructorPassword } = await import("../services/classroom.js");
125
+
126
+ authBtn.textContent = "驗證中...";
127
+ authBtn.disabled = true;
128
+
129
+ try {
130
+ const isValid = await verifyInstructorPassword(pwdInput.value);
131
+ if (isValid) {
132
+ authModal.classList.add('hidden');
133
+ // Store session to avoid re-login on reload (Optional, for now just per session)
134
+ } else {
135
+ alert('密碼錯誤');
136
+ pwdInput.value = '';
137
+ }
138
+ } catch (e) {
139
+ console.error(e);
140
+ alert("驗證出錯");
141
+ } finally {
142
+ authBtn.textContent = "確認進入";
143
+ authBtn.disabled = false;
144
  }
145
  };
146
 
src/views/StudentView.js CHANGED
@@ -3,52 +3,15 @@ import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPr
3
  // Cache challenges locally
4
  let cachedChallenges = [];
5
 
6
- export async function renderStudentView() {
7
- const nickname = localStorage.getItem('vibecoding_nickname') || 'Guest';
8
- const roomCode = localStorage.getItem('vibecoding_room_code') || 'Unknown';
9
- const userId = localStorage.getItem('vibecoding_user_id');
10
-
11
- // Fetch challenges if empty
12
- if (cachedChallenges.length === 0) {
13
- try {
14
- cachedChallenges = await getChallenges();
15
- } catch (e) {
16
- console.error("Failed to fetch challenges", e);
17
- throw new Error("無法讀取題目列表 (Error: " + e.message + ")");
18
- }
19
- }
20
-
21
- // Fetch User Progress
22
- let userProgress = {};
23
- if (userId) {
24
- try {
25
- userProgress = await getUserProgress(userId);
26
- } catch (e) {
27
- console.error("Failed to fetch progress", e);
28
- }
29
- }
30
 
31
- const levelGroups = {
32
- beginner: cachedChallenges.filter(c => c.level === 'beginner'),
33
- intermediate: cachedChallenges.filter(c => c.level === 'intermediate'),
34
- advanced: cachedChallenges.filter(c => c.level === 'advanced')
35
- };
36
-
37
- const levelNames = {
38
- beginner: "初級 (Beginner)",
39
- intermediate: "中級 (Intermediate)",
40
- advanced: "高級 (Advanced)"
41
- };
42
-
43
- function renderTaskCard(c) {
44
- const p = userProgress[c.id] || {};
45
- const isCompleted = p.status === 'completed';
46
- const isStarted = p.status === 'started';
47
-
48
- // 1. Completed State: Collapsed with Trophy
49
- if (isCompleted) {
50
- return `
51
- <div class="bg-gray-800/50 border border-green-500/30 rounded-xl p-4 flex items-center justify-between group hover:bg-gray-800 transition-all">
52
  <div class="flex items-center space-x-3">
53
  <div class="text-2xl">🏆</div>
54
  <h3 class="font-bold text-gray-300 group-hover:text-white transition-colors">${c.title}</h3>
@@ -69,21 +32,24 @@ export async function renderStudentView() {
69
  <div class="font-mono bg-black p-2 rounded text-gray-300 border border-gray-800">${p.submission_prompt}</div>
70
  </div>
71
  `;
72
- }
73
-
74
- // 2. Started or Not Started
75
- // If Started: Show "Prompt Input" area.
76
- // If Not Started: Show "Start Task" button only.
77
 
78
- return `
79
- <div class="group relative bg-gray-800 bg-opacity-50 border ${isStarted ? 'border-cyan-500/50 shadow-[0_0_15px_rgba(6,182,212,0.1)]' : 'border-gray-700'} rounded-2xl overflow-hidden hover:border-cyan-500/50 transition-all duration-300 flex flex-col">
 
80
  <div class="absolute top-0 left-0 w-1 h-full ${isStarted ? 'bg-cyan-500' : 'bg-gray-600'} group-hover:bg-cyan-400 transition-colors"></div>
81
 
82
  <div class="p-6 pl-8 flex-1 flex flex-col">
83
  <div class="flex flex-col sm:flex-row justify-between items-start mb-4 gap-4">
84
  <div>
85
  <h2 class="text-xl font-bold text-white mb-1">${c.title}</h2>
86
- <p class="text-gray-400 text-sm whitespace-pre-line">${c.description}</p>
 
 
 
 
 
 
87
  </div>
88
  </div>
89
 
@@ -123,12 +89,9 @@ export async function renderStudentView() {
123
  </div>
124
  </div>
125
  `;
126
- }
127
 
128
- // Accordion Layout
129
- return `
130
- <div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl">
131
- <header class="flex justify-between 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">
132
  <div class="flex flex-col">
133
  <div class="flex items-center space-x-2">
134
  <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
@@ -165,22 +128,24 @@ export async function renderStudentView() {
165
  </div>
166
  </summary>
167
  <div class="p-4 pt-0 grid grid-cols-1 gap-4 mt-4">
168
- ${tasks.length > 0 ? tasks.map(c => renderTaskCard(c)).join('') : '<div class="text-gray-500 text-sm italic">本區段尚無題目</div>'}
 
 
169
  </div>
170
  </details>
171
  `;
172
  }).join('')}
173
  </div>
174
 
175
- <!-- Peer Learning FAB -->
176
  <button onclick="window.openPeerModal()" class="fixed bottom-6 right-6 bg-purple-600 hover:bg-purple-500 text-white rounded-full p-4 shadow-xl shadow-purple-600/40 transition-transform hover:scale-110 active:scale-90 z-40"
177
  title="查看同學作業">
178
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
179
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0z" />
180
  </svg>
181
  </button>
182
- </div>
183
- `;
184
  }
185
 
186
  export function setupStudentEvents() {
@@ -210,8 +175,8 @@ export function setupStudentEvents() {
210
  };
211
 
212
  window.submitLevel = async (challengeId) => {
213
- const input = document.getElementById(`input-${challengeId}`);
214
- const errorMsg = document.getElementById(`error-${challengeId}`);
215
  const prompt = input.value;
216
  const roomCode = localStorage.getItem('vibecoding_room_code');
217
  const userId = localStorage.getItem('vibecoding_user_id');
@@ -240,11 +205,31 @@ export function setupStudentEvents() {
240
  btn.textContent = "✓ 已通關";
241
  btn.classList.add("bg-green-600");
242
 
243
- // Re-render to show Trophy state after short delay
244
- setTimeout(async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  const app = document.querySelector('#app');
246
  app.innerHTML = await renderStudentView();
247
- }, 1000);
248
 
249
  } catch (error) {
250
  console.error(error);
@@ -304,30 +289,30 @@ function renderPeerModal() {
304
  let optionsHtml = '<option value="" disabled selected>選擇題目...</option>';
305
  if (cachedChallenges.length > 0) {
306
  optionsHtml += cachedChallenges.map(c =>
307
- `<option value="${c.id}">[${c.level}] ${c.title}</option>`
308
  ).join('');
309
  }
310
 
311
  return `
312
- <div id="peer-modal" class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm hidden flex items-center justify-center z-50 p-4">
313
- <div class="bg-gray-800 rounded-2xl w-full max-w-md h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
314
- <div class="p-6 border-b border-gray-700 flex justify-between items-center">
315
- <h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
316
- <button onclick="closePeerModal()" class="text-gray-400 hover:text-white">✕</button>
317
- </div>
318
-
319
- <div class="p-4 bg-gray-900 border-b border-gray-700">
320
- <select id="peer-challenge-select" onchange="loadPeerPrompts(this.value)" class="w-full bg-gray-800 border border-gray-600 rounded p-2 text-white">
321
- ${optionsHtml}
322
- </select>
323
- </div>
324
 
325
- <div class="p-4 flex-1 overflow-y-auto space-y-4" id="peer-prompts-container">
326
- <div class="text-center text-gray-500 mt-10">請選擇一個題目來查看</div>
 
327
  </div>
328
- </div>
329
- </div>
330
- `;
331
  }
332
 
333
  window.openPeerModal = () => {
@@ -362,7 +347,7 @@ window.loadPeerPrompts = async (challengeId) => {
362
  const isLiked = p.likedBy && p.likedBy.includes(currentUserId);
363
 
364
  return `
365
- <div class="bg-gray-700/30 p-4 rounded-xl border border-gray-600">
366
  <div class="flex items-center justify-between mb-2">
367
  <div class="flex items-center space-x-2">
368
  <div class="w-6 h-6 rounded-full bg-cyan-600 flex items-center justify-center text-xs font-bold text-white">
@@ -381,11 +366,11 @@ window.loadPeerPrompts = async (challengeId) => {
381
  </button>
382
  </div>
383
  <p class="text-gray-300 font-mono text-sm bg-black/20 p-3 rounded-lg border border-gray-700/50 whitespace-pre-wrap">${p.prompt}</p>
384
- </div>
385
- `}).join('');
386
 
387
  // Attach challenge title for notification context
388
- window.currentPeerChallengeTitle = document.querySelector(`#peer-challenge-select option[value="${challengeId}"]`).text;
389
  };
390
 
391
  // Like Handler
 
3
  // Cache challenges locally
4
  let cachedChallenges = [];
5
 
6
+ function renderTaskCard(c, userProgress) {
7
+ const p = userProgress[c.id] || {};
8
+ const isCompleted = p.status === 'completed';
9
+ const isStarted = p.status === 'started';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ // 1. Completed State: Collapsed with Trophy
12
+ if (isCompleted) {
13
+ return `
14
+ <div id="card-${c.id}" class="bg-gray-800/50 border border-green-500/30 rounded-xl p-4 flex items-center justify-between group hover:bg-gray-800 transition-all">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  <div class="flex items-center space-x-3">
16
  <div class="text-2xl">🏆</div>
17
  <h3 class="font-bold text-gray-300 group-hover:text-white transition-colors">${c.title}</h3>
 
32
  <div class="font-mono bg-black p-2 rounded text-gray-300 border border-gray-800">${p.submission_prompt}</div>
33
  </div>
34
  `;
35
+ }
 
 
 
 
36
 
37
+ // 2. Started or Not Started
38
+ return `
39
+ <div id="card-${c.id}" class="group relative bg-gray-800 bg-opacity-50 border ${isStarted ? 'border-cyan-500/50 shadow-[0_0_15px_rgba(6,182,212,0.1)]' : 'border-gray-700'} rounded-2xl overflow-hidden hover:border-cyan-500/50 transition-all duration-300 flex flex-col">
40
  <div class="absolute top-0 left-0 w-1 h-full ${isStarted ? 'bg-cyan-500' : 'bg-gray-600'} group-hover:bg-cyan-400 transition-colors"></div>
41
 
42
  <div class="p-6 pl-8 flex-1 flex flex-col">
43
  <div class="flex flex-col sm:flex-row justify-between items-start mb-4 gap-4">
44
  <div>
45
  <h2 class="text-xl font-bold text-white mb-1">${c.title}</h2>
46
+ <div class="bg-blue-900/30 border border-blue-500/30 p-4 rounded-xl my-3 shadow-[inset_0_0_10px_rgba(59,130,246,0.1)]">
47
+ <div class="flex items-start space-x-2 text-cyan-300 mb-1">
48
+ <svg class="w-5 h-5 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
49
+ <span class="font-bold text-sm tracking-wider uppercase">任務說明</span>
50
+ </div>
51
+ <p class="text-gray-200 text-base font-medium whitespace-pre-line leading-relaxed pl-7">${c.description}</p>
52
+ </div>
53
  </div>
54
  </div>
55
 
 
89
  </div>
90
  </div>
91
  `;
92
+ }
93
 
94
+ export async function renderStudentView() { <header class="flex justify-between 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">
 
 
 
95
  <div class="flex flex-col">
96
  <div class="flex items-center space-x-2">
97
  <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
 
128
  </div>
129
  </summary>
130
  <div class="p-4 pt-0 grid grid-cols-1 gap-4 mt-4">
131
+ <div class="p-4 pt-0 grid grid-cols-1 gap-4 mt-4">
132
+ ${tasks.length > 0 ? tasks.map(c => renderTaskCard(c, userProgress)).join('') : '<div class="text-gray-500 text-sm italic">本區段尚無題目</div>'}
133
+ </div>
134
  </div>
135
  </details>
136
  `;
137
  }).join('')}
138
  </div>
139
 
140
+ <!--Peer Learning FAB-- >
141
  <button onclick="window.openPeerModal()" class="fixed bottom-6 right-6 bg-purple-600 hover:bg-purple-500 text-white rounded-full p-4 shadow-xl shadow-purple-600/40 transition-transform hover:scale-110 active:scale-90 z-40"
142
  title="查看同學作業">
143
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
144
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0z" />
145
  </svg>
146
  </button>
147
+ </div >
148
+ `;
149
  }
150
 
151
  export function setupStudentEvents() {
 
175
  };
176
 
177
  window.submitLevel = async (challengeId) => {
178
+ const input = document.getElementById(`input - ${ challengeId } `);
179
+ const errorMsg = document.getElementById(`error - ${ challengeId } `);
180
  const prompt = input.value;
181
  const roomCode = localStorage.getItem('vibecoding_room_code');
182
  const userId = localStorage.getItem('vibecoding_user_id');
 
205
  btn.textContent = "✓ 已通關";
206
  btn.classList.add("bg-green-600");
207
 
208
+ // NEW: Partial Update Strategy directly
209
+ // 1. Find the container
210
+ // The card is the great-great-grandparent of the button (button -> div -> div -> div -> card)
211
+ // Or simpler: give ID to card wrapper in renderTaskCard.
212
+ // Let's assume renderTaskCard now adds id="card-${c.id}"
213
+
214
+ // 2. Re-render just this card
215
+ const challenge = cachedChallenges.find(c => c.id === challengeId);
216
+ const newProgress = { [challengeId]: { status: 'completed', submission_prompt: prompt } };
217
+
218
+ // We need to merge with existing progress to pass to renderTaskCard?
219
+ // Actually renderTaskCard takes the whole userProgress map.
220
+ // But for this specific card, we can just pass a map containing just this one, because renderTaskCard(c, map) only looks up map[c.id].
221
+
222
+ const newCardHTML = renderTaskCard(challenge, newProgress);
223
+
224
+ // 3. Replace in DOM
225
+ const oldCard = document.getElementById(`card - ${ challengeId } `);
226
+ if (oldCard) {
227
+ oldCard.outerHTML = newCardHTML;
228
+ } else {
229
+ // Fallback if ID not found (should not happen if we update renderTaskCard)
230
  const app = document.querySelector('#app');
231
  app.innerHTML = await renderStudentView();
232
+ }
233
 
234
  } catch (error) {
235
  console.error(error);
 
289
  let optionsHtml = '<option value="" disabled selected>選擇題目...</option>';
290
  if (cachedChallenges.length > 0) {
291
  optionsHtml += cachedChallenges.map(c =>
292
+ `< option value = "${c.id}" > [${ c.level }] ${ c.title }</option > `
293
  ).join('');
294
  }
295
 
296
  return `
297
+ < div id = "peer-modal" class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm hidden flex items-center justify-center z-50 p-4" >
298
+ <div class="bg-gray-800 rounded-2xl w-full max-w-md h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
299
+ <div class="p-6 border-b border-gray-700 flex justify-between items-center">
300
+ <h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
301
+ <button onclick="closePeerModal()" class="text-gray-400 hover:text-white">✕</button>
302
+ </div>
303
+
304
+ <div class="p-4 bg-gray-900 border-b border-gray-700">
305
+ <select id="peer-challenge-select" onchange="loadPeerPrompts(this.value)" class="w-full bg-gray-800 border border-gray-600 rounded p-2 text-white">
306
+ ${optionsHtml}
307
+ </select>
308
+ </div>
309
 
310
+ <div class="p-4 flex-1 overflow-y-auto space-y-4" id="peer-prompts-container">
311
+ <div class="text-center text-gray-500 mt-10">請選擇一個題目來查看</div>
312
+ </div>
313
  </div>
314
+ </div >
315
+ `;
 
316
  }
317
 
318
  window.openPeerModal = () => {
 
347
  const isLiked = p.likedBy && p.likedBy.includes(currentUserId);
348
 
349
  return `
350
+ < div class="bg-gray-700/30 p-4 rounded-xl border border-gray-600" >
351
  <div class="flex items-center justify-between mb-2">
352
  <div class="flex items-center space-x-2">
353
  <div class="w-6 h-6 rounded-full bg-cyan-600 flex items-center justify-center text-xs font-bold text-white">
 
366
  </button>
367
  </div>
368
  <p class="text-gray-300 font-mono text-sm bg-black/20 p-3 rounded-lg border border-gray-700/50 whitespace-pre-wrap">${p.prompt}</p>
369
+ </div >
370
+ `}).join('');
371
 
372
  // Attach challenge title for notification context
373
+ window.currentPeerChallengeTitle = document.querySelector(`#peer - challenge - select option[value = "${challengeId}"]`).text;
374
  };
375
 
376
  // Like Handler