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:
140
frontend/src/services/adminApi.ts
Normal file
140
frontend/src/services/adminApi.ts
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user