Spaces:
Running
Running
| import { getChallenges, createChallenge, updateChallenge, deleteChallenge } from "../services/classroom.js"; | |
| import { checkInstructorPermission } from "../services/auth.js"; | |
| import { auth } from "../services/firebase.js"; | |
| export function renderAdminView() { | |
| return ` | |
| <div class="min-h-screen p-6 pb-20"> | |
| <header class="flex justify-between items-center mb-10 bg-gray-800 bg-opacity-50 p-4 rounded-xl border border-gray-700 backdrop-blur-sm"> | |
| <div class="flex items-center space-x-4"> | |
| <button id="back-instructor-btn" class="bg-gray-700 hover:bg-gray-600 text-white p-2 rounded-lg transition-all"> | |
| ← 回講師端 | |
| </button> | |
| <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-red-400 to-orange-600"> | |
| 後台管理系統 Admin Panel | |
| </h1> | |
| </div> | |
| <button id="add-challenge-btn" class="bg-green-600 hover:bg-green-500 text-white font-bold py-2 px-6 rounded-lg transition-all shadow-lg"> | |
| + 新增題目 | |
| </button> | |
| </header> | |
| <div id="challenges-list" class="space-y-4"> | |
| <!-- Questions loaded here --> | |
| <div class="text-center text-gray-500 py-10">載入中...</div> | |
| </div> | |
| <!-- Edit/Add Modal --> | |
| <div id="challenge-modal" class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm hidden flex items-center justify-center z-50 p-4"> | |
| <div class="bg-gray-800 rounded-xl w-full max-w-2xl border border-gray-700 shadow-2xl overflow-y-auto max-h-[90vh]"> | |
| <div class="p-6 border-b border-gray-700"> | |
| <h3 id="modal-title" class="text-xl font-bold text-white">編輯題目</h3> | |
| </div> | |
| <div class="p-6 space-y-4"> | |
| <input type="hidden" id="edit-id"> | |
| <div> | |
| <label class="block text-gray-400 mb-1">標題 (Title)</label> | |
| <input type="text" id="edit-title" class="w-full bg-gray-900 border border-gray-600 rounded p-2 text-white"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-400 mb-1">難度 (Level)</label> | |
| <select id="edit-level" class="w-full bg-gray-900 border border-gray-600 rounded p-2 text-white"> | |
| <option value="beginner">初級 (Beginner)</option> | |
| <option value="intermediate">中級 (Intermediate)</option> | |
| <option value="advanced">高級 (Advanced)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-gray-400 mb-1">描述 (Description)</label> | |
| <textarea id="edit-desc" rows="3" class="w-full bg-gray-900 border border-gray-600 rounded p-2 text-white"></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-gray-400 mb-1">連結 (GeminiCanvas Link/Code)</label> | |
| <input type="text" id="edit-link" class="w-full bg-gray-900 border border-gray-600 rounded p-2 text-white"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-400 mb-1">排序 (Order)</label> | |
| <input type="number" id="edit-order" class="w-full bg-gray-900 border border-gray-600 rounded p-2 text-white" value="1"> | |
| </div> | |
| </div> | |
| <div class="p-6 border-t border-gray-700 flex justify-end space-x-3"> | |
| <button onclick="closeChallengeModal()" class="px-4 py-2 text-gray-400 hover:text-white">取消</button> | |
| <button id="save-challenge-btn" class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded">儲存</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| export function setupAdminEvents() { | |
| // Permission Check | |
| const user = auth.currentUser; | |
| if (!user) { | |
| alert("請先登入"); | |
| window.location.hash = ''; // Back to Landing | |
| return; | |
| } | |
| checkInstructorPermission(user).then(inst => { | |
| if (!inst || !inst.permissions?.includes('add_question')) { | |
| alert("您沒有權限管理題目"); | |
| window.location.hash = 'instructor'; | |
| return; | |
| } | |
| }); | |
| loadChallenges(); | |
| document.getElementById('back-instructor-btn').addEventListener('click', () => { | |
| const referer = localStorage.getItem('vibecoding_admin_referer'); | |
| if (referer === 'instructor') { | |
| window.location.hash = 'instructor'; | |
| } else { | |
| window.location.hash = ''; // Main landing | |
| } | |
| localStorage.removeItem('vibecoding_admin_referer'); | |
| }); | |
| document.getElementById('add-challenge-btn').addEventListener('click', () => { | |
| openModal(); | |
| }); | |
| document.getElementById('save-challenge-btn').addEventListener('click', async () => { | |
| const id = document.getElementById('edit-id').value; | |
| const data = { | |
| title: document.getElementById('edit-title').value, | |
| level: document.getElementById('edit-level').value, | |
| description: document.getElementById('edit-desc').value, | |
| link: document.getElementById('edit-link').value, | |
| order: parseInt(document.getElementById('edit-order').value) || 0 | |
| }; | |
| if (id) { | |
| await updateChallenge(id, data); | |
| } else { | |
| await createChallenge(data); | |
| } | |
| closeChallengeModal(); | |
| loadChallenges(); | |
| }); | |
| } | |
| async function loadChallenges() { | |
| const list = document.getElementById('challenges-list'); | |
| const challenges = await getChallenges(); | |
| const levels = ['beginner', 'intermediate', 'advanced']; | |
| const levelNames = { | |
| beginner: "初級 (Beginner)", | |
| intermediate: "中級 (Intermediate)", | |
| advanced: "高級 (Advanced)" | |
| }; | |
| // Group challenges | |
| const groups = { beginner: [], intermediate: [], advanced: [] }; | |
| challenges.forEach(c => { | |
| if (groups[c.level]) groups[c.level].push(c); | |
| else groups.beginner.push(c); // Fallback | |
| }); | |
| list.innerHTML = levels.map(level => { | |
| const groupItems = groups[level] || []; | |
| const isOpen = level === 'beginner' ? 'open' : ''; // Open first by default | |
| const itemsHtml = groupItems.map(c => ` | |
| <div class="bg-gray-800 p-4 rounded-lg border border-gray-700 flex justify-between items-center group hover:border-gray-500 transition-colors mb-2"> | |
| <div class="flex items-center space-x-3"> | |
| <span class="text-gray-500 font-mono text-sm">#${c.order}</span> | |
| <div> | |
| <span class="font-bold text-white text-lg">${c.title}</span> | |
| <p class="text-gray-400 text-sm mt-1 truncate max-w-md">${c.description}</p> | |
| </div> | |
| </div> | |
| <div class="flex space-x-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <button onclick="window.editChallenge('${c.id}')" class="bg-cyan-600/20 text-cyan-400 px-3 py-1 rounded hover:bg-cyan-600 hover:text-white transition-colors">編輯</button> | |
| <button onclick="window.deleteChallenge('${c.id}')" class="bg-red-600/20 text-red-400 px-3 py-1 rounded hover:bg-red-600 hover:text-white transition-colors">刪除</button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| return ` | |
| <details class="group border border-gray-700 rounded-xl bg-gray-800/30 overflow-hidden mb-4" ${isOpen}> | |
| <summary class="flex items-center justify-between p-4 cursor-pointer bg-gray-800 hover:bg-gray-700 transition-colors select-none"> | |
| <div class="flex items-center space-x-3"> | |
| <h3 class="text-xl font-bold text-cyan-400">${levelNames[level]}</h3> | |
| <span class="text-xs text-gray-400 bg-gray-900 px-2 py-1 rounded-full">${groupItems.length} 題</span> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button onclick="event.preventDefault(); window.openModal(null, '${level}')" class="bg-green-600 hover:bg-green-500 text-white text-xs font-bold py-1 px-3 rounded transition-colors shadow-lg"> | |
| + 新增至此區 | |
| </button> | |
| <svg class="w-5 h-5 text-gray-400 transform group-open:rotate-180 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </div> | |
| </summary> | |
| <div class="p-4 pt-4 border-t border-gray-700/50"> | |
| ${itemsHtml || '<div class="text-gray-500 text-center italic">尚無題目,請新增</div>'} | |
| </div> | |
| </details> | |
| `; | |
| }).join(''); | |
| // Expose helpers globally for onclick | |
| window.editChallenge = (id) => { | |
| const c = challenges.find(x => x.id === id); | |
| if (c) openModal(c); | |
| }; | |
| window.deleteChallenge = async (id) => { | |
| if (confirm('確定刪除?')) { | |
| await deleteChallenge(id); | |
| loadChallenges(); | |
| } | |
| }; | |
| } | |
| // Global expose for the "Add to Section" button | |
| window.openModal = function (challenge = null, defaultLevel = 'beginner') { | |
| const modal = document.getElementById('challenge-modal'); | |
| const title = document.getElementById('modal-title'); | |
| // Reset or Fill | |
| document.getElementById('edit-id').value = challenge ? challenge.id : ''; | |
| document.getElementById('edit-title').value = challenge ? challenge.title : ''; | |
| // Use challenge level if editing, otherwise use defaultLevel passed from section button | |
| document.getElementById('edit-level').value = challenge ? challenge.level : defaultLevel; | |
| document.getElementById('edit-desc').value = challenge ? challenge.description : ''; | |
| document.getElementById('edit-link').value = challenge ? challenge.link : ''; | |
| document.getElementById('edit-order').value = challenge ? challenge.order : '1'; | |
| title.textContent = challenge ? '編輯題目' : '新增題目'; | |
| modal.classList.remove('hidden'); | |
| } | |
| window.closeChallengeModal = () => { | |
| document.getElementById('challenge-modal').classList.add('hidden'); | |
| }; | |