import { useState, useCallback, useRef } from 'react'; import toast from 'react-hot-toast'; import { useAuthenticatedFetch } from './useAuthenticatedFetch'; import type { Quiz, QuizSource, SavedQuiz, QuizListItem, GameConfig } from '../types'; interface UseQuizLibraryReturn { quizzes: QuizListItem[]; loading: boolean; loadingQuizId: string | null; deletingQuizId: string | null; saving: boolean; error: string | null; fetchQuizzes: () => Promise; loadQuiz: (id: string) => Promise; saveQuiz: (quiz: Quiz, source: QuizSource, aiTopic?: string) => Promise; updateQuiz: (id: string, quiz: Quiz) => Promise; updateQuizConfig: (id: string, config: GameConfig) => Promise; deleteQuiz: (id: string) => Promise; retry: () => Promise; clearError: () => void; } export const useQuizLibrary = (): UseQuizLibraryReturn => { const { authFetch, isAuthenticated } = useAuthenticatedFetch(); const [quizzes, setQuizzes] = useState([]); const [loading, setLoading] = useState(false); const [loadingQuizId, setLoadingQuizId] = useState(null); const [deletingQuizId, setDeletingQuizId] = useState(null); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const lastOperationRef = useRef<(() => Promise) | null>(null); const savingRef = useRef(false); const fetchQuizzes = useCallback(async () => { if (!isAuthenticated) return; setLoading(true); setError(null); lastOperationRef.current = fetchQuizzes; try { const response = await authFetch('/api/quizzes'); if (!response.ok) { const errorText = response.status === 500 ? 'Server error. Please try again.' : 'Failed to load your quizzes.'; throw new Error(errorText); } const data = await response.json(); setQuizzes(data); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to load quizzes'; setError(message); if (!message.includes('redirecting')) { toast.error(message); } } finally { setLoading(false); } }, [authFetch, isAuthenticated]); const loadQuiz = useCallback(async (id: string): Promise => { setLoadingQuizId(id); setError(null); try { const response = await authFetch(`/api/quizzes/${id}`); if (!response.ok) { const errorText = response.status === 404 ? 'Quiz not found. It may have been deleted.' : 'Failed to load quiz.'; throw new Error(errorText); } const data = await response.json(); toast.success('Quiz loaded!'); return { ...data, config: data.gameConfig, }; } catch (err) { const message = err instanceof Error ? err.message : 'Failed to load quiz'; if (!message.includes('redirecting')) { toast.error(message); } throw err; } finally { setLoadingQuizId(null); } }, [authFetch]); const saveQuiz = useCallback(async ( quiz: Quiz, source: QuizSource, aiTopic?: string ): Promise => { if (savingRef.current) { toast.error('Save already in progress'); throw new Error('Save already in progress'); } if (!quiz.title?.trim()) { toast.error('Quiz must have a title'); throw new Error('Quiz must have a title'); } if (!quiz.questions || quiz.questions.length === 0) { toast.error('Quiz must have at least one question'); throw new Error('Quiz must have at least one question'); } for (const q of quiz.questions) { if (!q.text?.trim()) { toast.error('All questions must have text'); throw new Error('All questions must have text'); } if (!q.options || q.options.length < 2) { toast.error('Each question must have at least 2 options'); throw new Error('Each question must have at least 2 options'); } const hasCorrect = q.options.some(o => o.isCorrect); if (!hasCorrect) { toast.error('Each question must have a correct answer'); throw new Error('Each question must have a correct answer'); } } savingRef.current = true; setSaving(true); setError(null); try { const response = await authFetch('/api/quizzes', { method: 'POST', body: JSON.stringify({ title: quiz.title, source, aiTopic, gameConfig: quiz.config, questions: quiz.questions.map(q => ({ text: q.text, timeLimit: q.timeLimit, options: q.options.map(o => ({ text: o.text, isCorrect: o.isCorrect, shape: o.shape, color: o.color, reason: o.reason, })), })), }), }); if (!response.ok) { const errorText = response.status === 400 ? 'Invalid quiz data. Please check and try again.' : 'Failed to save quiz.'; throw new Error(errorText); } const data = await response.json(); toast.success('Quiz saved to your library!'); return data.id; } catch (err) { const message = err instanceof Error ? err.message : 'Failed to save quiz'; if (!message.includes('redirecting')) { toast.error(message); } throw err; } finally { setSaving(false); } }, [authFetch]); const updateQuiz = useCallback(async (id: string, quiz: Quiz): Promise => { setSaving(true); setError(null); try { const response = await authFetch(`/api/quizzes/${id}`, { method: 'PUT', body: JSON.stringify({ title: quiz.title, gameConfig: quiz.config, questions: quiz.questions.map(q => ({ text: q.text, timeLimit: q.timeLimit, options: q.options.map(o => ({ text: o.text, isCorrect: o.isCorrect, shape: o.shape, color: o.color, reason: o.reason, })), })), }), }); if (!response.ok) { const errorText = response.status === 404 ? 'Quiz not found.' : 'Failed to update quiz.'; throw new Error(errorText); } toast.success('Quiz updated!'); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to update quiz'; if (!message.includes('redirecting')) { toast.error(message); } throw err; } finally { setSaving(false); } }, [authFetch]); const updateQuizConfig = useCallback(async (id: string, config: GameConfig): Promise => { try { const response = await authFetch(`/api/quizzes/${id}/config`, { method: 'PATCH', body: JSON.stringify({ gameConfig: config }), }); if (!response.ok) { throw new Error('Failed to update config'); } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to update config'; if (!message.includes('redirecting')) { toast.error(message); } throw err; } }, [authFetch]); const deleteQuiz = useCallback(async (id: string): Promise => { setDeletingQuizId(id); setError(null); try { const response = await authFetch(`/api/quizzes/${id}`, { method: 'DELETE', }); if (!response.ok && response.status !== 204) { const errorText = response.status === 404 ? 'Quiz not found.' : 'Failed to delete quiz.'; throw new Error(errorText); } setQuizzes(prev => prev.filter(q => q.id !== id)); toast.success('Quiz deleted'); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to delete quiz'; if (!message.includes('redirecting')) { toast.error(message); } throw err; } finally { setDeletingQuizId(null); } }, [authFetch]); const retry = useCallback(async () => { if (lastOperationRef.current) { await lastOperationRef.current(); } }, []); const clearError = useCallback(() => { setError(null); }, []); return { quizzes, loading, loadingQuizId, deletingQuizId, saving, error, fetchQuizzes, loadQuiz, saveQuiz, updateQuiz, updateQuizConfig, deleteQuiz, retry, clearError, }; };