Phase 6 complete
This commit is contained in:
parent
93ea01525e
commit
3a22b42492
11 changed files with 735 additions and 103 deletions
|
|
@ -1,49 +1,84 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useAuthenticatedFetch } from './useAuthenticatedFetch';
|
||||
import type { Quiz, QuizSource, SavedQuiz, QuizListItem } from '../types';
|
||||
|
||||
interface UseQuizLibraryReturn {
|
||||
quizzes: QuizListItem[];
|
||||
loading: boolean;
|
||||
loadingQuizId: string | null;
|
||||
deletingQuizId: string | null;
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
fetchQuizzes: () => Promise<void>;
|
||||
loadQuiz: (id: string) => Promise<SavedQuiz>;
|
||||
saveQuiz: (quiz: Quiz, source: QuizSource, aiTopic?: string) => Promise<string>;
|
||||
deleteQuiz: (id: string) => Promise<void>;
|
||||
retry: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useQuizLibrary = (): UseQuizLibraryReturn => {
|
||||
const { authFetch, isAuthenticated } = useAuthenticatedFetch();
|
||||
const [quizzes, setQuizzes] = useState<QuizListItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingQuizId, setLoadingQuizId] = useState<string | null>(null);
|
||||
const [deletingQuizId, setDeletingQuizId] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const lastOperationRef = useRef<(() => Promise<void>) | null>(null);
|
||||
|
||||
const fetchQuizzes = useCallback(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
lastOperationRef.current = fetchQuizzes;
|
||||
|
||||
try {
|
||||
const response = await authFetch('/api/quizzes');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch quizzes');
|
||||
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) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch quizzes');
|
||||
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<SavedQuiz> => {
|
||||
const response = await authFetch(`/api/quizzes/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load quiz');
|
||||
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);
|
||||
}
|
||||
toast.success('Quiz loaded!');
|
||||
return response.json();
|
||||
} 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);
|
||||
}
|
||||
return response.json();
|
||||
}, [authFetch]);
|
||||
|
||||
const saveQuiz = useCallback(async (
|
||||
|
|
@ -51,53 +86,131 @@ export const useQuizLibrary = (): UseQuizLibraryReturn => {
|
|||
source: QuizSource,
|
||||
aiTopic?: string
|
||||
): Promise<string> => {
|
||||
const response = await authFetch('/api/quizzes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: quiz.title,
|
||||
source,
|
||||
aiTopic,
|
||||
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) {
|
||||
throw new Error('Failed to save quiz');
|
||||
if (saving) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await authFetch('/api/quizzes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: quiz.title,
|
||||
source,
|
||||
aiTopic,
|
||||
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,
|
||||
})),
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.id;
|
||||
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 deleteQuiz = useCallback(async (id: string): Promise<void> => {
|
||||
const response = await authFetch(`/api/quizzes/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
setDeletingQuizId(id);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await authFetch(`/api/quizzes/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 204) {
|
||||
throw new Error('Failed to delete quiz');
|
||||
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);
|
||||
}
|
||||
|
||||
setQuizzes(prev => prev.filter(q => q.id !== id));
|
||||
}, [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,
|
||||
deleteQuiz,
|
||||
retry,
|
||||
clearError,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue