feat(phase5): Add admin panel with JWT auth, questions CRUD, and calendar

- Add adminStore with Zustand for authentication state persistence
- Add adminApi service for all admin endpoints
- Add Login page with error handling and redirect
- Add AdminLayout with sidebar navigation and route protection
- Add Dashboard with stats and quick actions
- Add Questions page with full CRUD, filters, and AI generation modal
- Add Calendar page for scheduling questions by date
- Integrate admin routes in App.tsx with nested routing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 08:49:28 +00:00
parent 3e91305e46
commit 90fa220890
9 changed files with 1205 additions and 1 deletions

View File

@@ -0,0 +1,140 @@
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
// Helper para obtener headers con auth
const getAuthHeaders = (token: string | null) => ({
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
})
// Auth
export const adminLogin = async (username: string, password: string) => {
const formData = new FormData()
formData.append('username', username)
formData.append('password', password)
const response = await fetch(`${API_URL}/api/admin/login`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || 'Login failed')
}
return response.json()
}
export const adminRegister = async (username: string, password: string) => {
const response = await fetch(`${API_URL}/api/admin/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || 'Registration failed')
}
return response.json()
}
// Questions
export const getQuestions = async (token: string, categoryId?: number, status?: string) => {
const params = new URLSearchParams()
if (categoryId) params.append('category_id', String(categoryId))
if (status) params.append('status', status)
const response = await fetch(`${API_URL}/api/admin/questions?${params}`, {
headers: getAuthHeaders(token)
})
if (!response.ok) throw new Error('Failed to fetch questions')
return response.json()
}
export const createQuestion = async (token: string, data: {
category_id: number
question_text: string
correct_answer: string
alt_answers?: string[]
difficulty: number
date_active?: string
fun_fact?: string
}) => {
const response = await fetch(`${API_URL}/api/admin/questions`, {
method: 'POST',
headers: getAuthHeaders(token),
body: JSON.stringify(data)
})
if (!response.ok) throw new Error('Failed to create question')
return response.json()
}
export const updateQuestion = async (token: string, id: number, data: Record<string, unknown>) => {
const response = await fetch(`${API_URL}/api/admin/questions/${id}`, {
method: 'PUT',
headers: getAuthHeaders(token),
body: JSON.stringify(data)
})
if (!response.ok) throw new Error('Failed to update question')
return response.json()
}
export const deleteQuestion = async (token: string, id: number) => {
const response = await fetch(`${API_URL}/api/admin/questions/${id}`, {
method: 'DELETE',
headers: getAuthHeaders(token)
})
if (!response.ok) throw new Error('Failed to delete question')
return response.json()
}
export const generateQuestions = async (token: string, data: {
category_id: number
difficulty: number
count: number
}) => {
const response = await fetch(`${API_URL}/api/admin/questions/generate`, {
method: 'POST',
headers: getAuthHeaders(token),
body: JSON.stringify(data)
})
if (!response.ok) throw new Error('Failed to generate questions')
return response.json()
}
export const approveQuestion = async (token: string, id: number) => {
const response = await fetch(`${API_URL}/api/admin/questions/${id}/approve`, {
method: 'POST',
headers: getAuthHeaders(token)
})
if (!response.ok) throw new Error('Failed to approve question')
return response.json()
}
export const rejectQuestion = async (token: string, id: number) => {
const response = await fetch(`${API_URL}/api/admin/questions/${id}/reject`, {
method: 'POST',
headers: getAuthHeaders(token)
})
if (!response.ok) throw new Error('Failed to reject question')
return response.json()
}
// Categories
export const getCategories = async (token: string) => {
const response = await fetch(`${API_URL}/api/admin/categories`, {
headers: getAuthHeaders(token)
})
if (!response.ok) throw new Error('Failed to fetch categories')
return response.json()
}