Spaces:
Running
Running
| import { createRoom, joinRoom, checkNicknameConflict } from "../services/classroom.js"; | |
| import { generateMonsterSVG, MONSTER_DEFS } from "../utils/monsterUtils.js"; | |
| // ... (renderLandingView remains same) ... | |
| export function renderLandingView() { | |
| // Select Decor Monsters | |
| // Left: Genesis Dragon (L3_AAA), Right: Gundam (L3_BAA) - or fallbacks | |
| const mLeft = MONSTER_DEFS.find(m => m.id === 'L3_AAA') || MONSTER_DEFS.find(m => m.stage === 3); | |
| const mRight = MONSTER_DEFS.find(m => m.id === 'L3_BAA') || MONSTER_DEFS.find(m => m.stage === 3); | |
| const svgLeft = generateMonsterSVG(mLeft); | |
| const svgRight = generateMonsterSVG(mRight); | |
| return ` | |
| <div class="min-h-screen flex flex-col items-center justify-center p-4 relative overflow-hidden"> | |
| <!-- Decor Monsters (Desktop Only) --> | |
| <div class="absolute bottom-10 left-10 w-48 h-48 hidden lg:block pointer-events-none" | |
| style="animation: float 6s ease-in-out infinite;"> | |
| <div class="w-full h-full drop-shadow-[0_0_15px_rgba(34,211,238,0.5)]"> | |
| ${svgLeft} | |
| </div> | |
| </div> | |
| <div class="absolute bottom-10 right-10 w-48 h-48 hidden lg:block pointer-events-none" | |
| style="animation: float 8s ease-in-out infinite reverse;"> | |
| <div class="w-full h-full drop-shadow-[0_0_15px_rgba(59,130,246,0.5)]"> | |
| ${svgRight} | |
| </div> | |
| </div> | |
| <div class="max-w-md w-full bg-gray-600 bg-opacity-20 backdrop-blur-lg rounded-xl shadow-2xl p-8 border border-gray-700 z-10"> | |
| <h1 class="text-3xl sm:text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-purple-500 mb-8 text-center tracking-tighter whitespace-nowrap"> | |
| VIBECODING-怪獸成長營 | |
| </h1> | |
| <!-- Student Join Form --> | |
| <div id="student-form" class="space-y-6"> | |
| <div> | |
| <label class="block text-gray-400 text-sm font-bold mb-2">教室代碼 (Room Code)</label> | |
| <input type="text" id="room-code-input" class="w-full bg-gray-800 text-white border border-gray-600 rounded-lg py-3 px-4 focus:outline-none focus:border-cyan-500 transition-colors" placeholder="1234"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-400 text-sm font-bold mb-2">您的暱稱 (Nickname)</label> | |
| <input type="text" id="nickname-input" class="w-full bg-gray-800 text-white border border-gray-600 rounded-lg py-3 px-4 focus:outline-none focus:border-purple-500 transition-colors" placeholder="小明"> | |
| </div> | |
| <button id="join-btn" class="w-full bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white font-bold py-3 px-4 rounded-lg transform transition hover:scale-105 active:scale-95 shadow-lg shadow-cyan-500/30"> | |
| 進入教室 | |
| </button> | |
| </div> | |
| <!-- Instructor Toggle --> | |
| <div class="mt-8 pt-6 border-t border-gray-700 text-center"> | |
| <button id="instructor-mode-btn" class="text-gray-500 text-sm hover:text-cyan-400 transition-colors"> | |
| 我是講師 (Instructor Mode) | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Conflict Modal Container --> | |
| <div id="conflict-modal-container"></div> | |
| <style> | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-20px); } | |
| } | |
| </style> | |
| </div> | |
| `; | |
| } | |
| export function setupLandingEvents(navigateTo) { | |
| const joinBtn = document.getElementById('join-btn'); | |
| const instructorBtn = document.getElementById('instructor-mode-btn'); | |
| const handleJoin = async (roomCode, nickname, forceNew = false) => { | |
| try { | |
| const { userId, nickname: finalNickname } = await joinRoom(roomCode, nickname, forceNew); | |
| // Save Session | |
| localStorage.setItem('vibecoding_user_id', userId); | |
| localStorage.setItem('vibecoding_room_code', roomCode); | |
| localStorage.setItem('vibecoding_nickname', finalNickname); | |
| navigateTo('student'); | |
| return true; | |
| } catch (error) { | |
| alert('加入失敗: ' + error.message); | |
| return false; | |
| } | |
| }; | |
| joinBtn.addEventListener('click', async () => { | |
| const roomCode = document.getElementById('room-code-input').value.trim(); | |
| const nickname = document.getElementById('nickname-input').value.trim(); | |
| if (!roomCode || !nickname) { | |
| alert('請輸入教室代碼和暱稱'); | |
| return; | |
| } | |
| joinBtn.textContent = '檢查中...'; | |
| joinBtn.disabled = true; | |
| try { | |
| // Check conflicts first | |
| const conflicts = await checkNicknameConflict(roomCode, nickname); | |
| if (conflicts.length > 0) { | |
| // Show Conflict Modal | |
| showConflictModal(conflicts, nickname, roomCode, navigateTo, handleJoin); | |
| joinBtn.textContent = '進入教室'; | |
| joinBtn.disabled = false; | |
| return; | |
| } | |
| // No conflict -> direct join | |
| await handleJoin(roomCode, nickname); | |
| } catch (e) { | |
| console.error(e); | |
| alert("檢查失敗: " + e.message); | |
| joinBtn.textContent = '進入教室'; | |
| joinBtn.disabled = false; | |
| } | |
| }); | |
| instructorBtn.addEventListener('click', () => { | |
| // Clear any previous admin referer to ensure clean state | |
| localStorage.removeItem('vibecoding_admin_referer'); | |
| navigateTo('instructor'); | |
| }); | |
| } | |
| function showConflictModal(conflicts, originalNickname, roomCode, navigateTo, handleJoin) { | |
| const container = document.getElementById('conflict-modal-container'); | |
| const userListHTML = conflicts.map(u => ` | |
| <button onclick="window.selectUser('${u.nickname}')" | |
| class="w-full text-left bg-gray-700 hover:bg-gray-600 p-4 rounded-xl flex justify-between items-center group transition-all border border-gray-600 hover:border-cyan-500"> | |
| <span class="font-bold text-white group-hover:text-cyan-300 transition-colors">${u.nickname}</span> | |
| <span class="text-xs text-gray-400 bg-gray-800 px-2 py-1 rounded">舊學員</span> | |
| </button> | |
| `).join(''); | |
| container.innerHTML = ` | |
| <div class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"> | |
| <div class="bg-gray-800 w-full max-w-md rounded-2xl border border-gray-700 shadow-2xl overflow-hidden animate-[fadeIn_0.3s_ease-out]"> | |
| <div class="p-6 border-b border-gray-700 bg-gray-900/50"> | |
| <h3 class="text-xl font-bold text-white mb-1">發現相同暱稱</h3> | |
| <p class="text-gray-400 text-sm">教室裡已經有叫「${originalNickname}」的同學了,請問您是?</p> | |
| </div> | |
| <div class="p-6 space-y-3 max-h-[60vh] overflow-y-auto"> | |
| ${userListHTML} | |
| <div class="relative flex py-2 items-center"> | |
| <div class="flex-grow border-t border-gray-700"></div> | |
| <span class="flex-shrink-0 mx-4 text-gray-500 text-xs">或者</span> | |
| <div class="flex-grow border-t border-gray-700"></div> | |
| </div> | |
| <button onclick="window.createNewUser()" | |
| class="w-full bg-gradient-to-r from-green-600 to-teal-600 hover:from-green-500 hover:to-teal-500 text-white font-bold p-4 rounded-xl shadow-lg shadow-green-900/40 transform transition hover:scale-[1.02] flex items-center justify-center space-x-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" /> | |
| </svg> | |
| <span>我是新同學,建立新分身</span> | |
| </button> | |
| </div> | |
| <div class="p-4 bg-gray-900/50 border-t border-gray-700 flex justify-end"> | |
| <button onclick="document.getElementById('conflict-modal-container').innerHTML=''" | |
| class="text-gray-400 hover:text-white text-sm px-4 py-2"> | |
| 取消 | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Bind temporary window functions for the modal buttons | |
| window.selectUser = async (targetNickname) => { | |
| // Log in as existing | |
| document.getElementById('conflict-modal-container').innerHTML = ''; // Close modal | |
| await handleJoin(roomCode, targetNickname, false); | |
| }; | |
| window.createNewUser = async () => { | |
| // Create new | |
| document.getElementById('conflict-modal-container').innerHTML = ''; // Close modal | |
| await handleJoin(roomCode, originalNickname, true); // Force new | |
| }; | |
| } | |