Vibecodingex / src /views /LandingView.js
Lashtw's picture
Upload 9 files
ae79a68 verified
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
};
}