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