"use client"; import React, { createContext, useContext, useState, useEffect, ReactNode, } from "react"; import { useRouter } from "next/navigation"; import { Exam, ExamAnswer, ExamAttempt, ExamContextType } from "@/types/exam"; import { getFromStorage, removeFromStorage, setToStorage } from "@/lib/utils"; const ExamContext = createContext(undefined); const STORAGE_KEYS = { CURRENT_EXAM: "current-exam", CURRENT_ATTEMPT: "current-attempt", } as const; export const ExamProvider: React.FC<{ children: ReactNode }> = ({ children, }) => { const router = useRouter(); const [currentExam, setCurrentExamState] = useState(null); const [currentAttempt, setCurrentAttemptState] = useState( null ); const [isHydrated, setIsHydrated] = useState(false); const [isInitialized, setIsInitialized] = useState(false); // Hydrate from session storage on mount useEffect(() => { const savedExam = getFromStorage(STORAGE_KEYS.CURRENT_EXAM); const savedAttempt = getFromStorage( STORAGE_KEYS.CURRENT_ATTEMPT ); if (savedExam) { setCurrentExamState(savedExam); } if (savedAttempt) { // Convert date strings back to Date objects const hydratedAttempt = { ...savedAttempt, startTime: new Date(savedAttempt.startTime), endTime: savedAttempt.endTime ? new Date(savedAttempt.endTime) : undefined, answers: savedAttempt.answers.map((answer) => ({ ...answer, timestamp: new Date(answer.timestamp), })), }; setCurrentAttemptState(hydratedAttempt); } setIsHydrated(true); }, []); // Persist to session storage whenever state changes useEffect(() => { if (!isHydrated) return; if (currentExam) { setToStorage(STORAGE_KEYS.CURRENT_EXAM, currentExam); } else { removeFromStorage(STORAGE_KEYS.CURRENT_EXAM); } }, [currentExam, isHydrated]); useEffect(() => { if (!isHydrated) return; if (currentAttempt) { setToStorage(STORAGE_KEYS.CURRENT_ATTEMPT, currentAttempt); } else { removeFromStorage(STORAGE_KEYS.CURRENT_ATTEMPT); } }, [currentAttempt, isHydrated]); const setCurrentExam = (exam: Exam) => { setCurrentExamState(exam); setCurrentAttemptState(null); }; const startExam = () => { if (!currentExam) { console.warn("No exam selected, redirecting to /unit"); router.push("/unit"); return; } const attempt: ExamAttempt = { examId: currentExam.id, exam: currentExam, answers: [], startTime: new Date(), }; setCurrentAttemptState(attempt); setIsInitialized(true); }; const setAnswer = (questionId: string, answer: any) => { if (!currentAttempt) { console.warn("No exam attempt started, redirecting to /unit"); router.push("/unit"); return; } setCurrentAttemptState((prev) => { if (!prev) return null; const existingAnswerIndex = prev.answers.findIndex( (a) => a.questionId === questionId ); const newAnswer: ExamAnswer = { questionId, answer, timestamp: new Date(), }; let newAnswers: ExamAnswer[]; if (existingAnswerIndex >= 0) { // Update existing answer newAnswers = [...prev.answers]; newAnswers[existingAnswerIndex] = newAnswer; } else { // Add new answer newAnswers = [...prev.answers, newAnswer]; } return { ...prev, answers: newAnswers, }; }); }; const setApiResponse = (response: any) => { if (!currentAttempt) { console.warn("No exam attempt started, redirecting to /unit"); router.push("/unit"); return; } setCurrentAttemptState((prev) => { if (!prev) return null; return { ...prev, apiResponse: response, }; }); }; const submitExam = (): ExamAttempt | null => { if (!currentAttempt) { console.warn("No exam attempt to submit, redirecting to /unit"); router.push("/unit"); return null; } // Calculate score (simple example - you can customize this) const attemptQuestions = currentAttempt.exam.questions; const totalQuestions = attemptQuestions.length; const answeredQuestions = currentAttempt.answers.filter( (a) => a.questionId !== "__api_response__" ).length; const score = Math.round((answeredQuestions / totalQuestions) * 100); const completedAttempt: ExamAttempt = { ...currentAttempt, endTime: new Date(), score, totalQuestions, passed: currentAttempt.exam.passingScore ? score >= currentAttempt.exam.passingScore : undefined, }; setCurrentAttemptState(completedAttempt); return completedAttempt; }; const clearExam = (): void => { setCurrentExamState(null); setCurrentAttemptState(null); }; const getAnswer = (questionId: string): any => { if (!currentAttempt) return undefined; const answer = currentAttempt.answers.find( (a) => a.questionId === questionId ); return answer?.answer; }; const getApiResponse = (): any => { return currentAttempt?.apiResponse; }; const getProgress = (): number => { if (!currentAttempt || !currentAttempt.exam) return 0; const totalQuestions = currentAttempt.exam.questions.length; const answeredQuestions = currentAttempt.answers.filter( (a) => a.questionId !== "__api_response__" ).length; return totalQuestions > 0 ? (answeredQuestions / totalQuestions) * 100 : 0; }; const isExamStarted = () => !!currentExam && !!currentAttempt; const isExamCompleted = (): boolean => { if (!isHydrated) return false; // wait for hydration return currentAttempt !== null && currentAttempt.endTime !== undefined; }; const contextValue: ExamContextType = { currentExam, currentAttempt, setCurrentExam, startExam, setAnswer, submitExam, clearExam, setApiResponse, getAnswer, getProgress, isExamStarted, isExamCompleted, getApiResponse, isHydrated, isInitialized, }; return ( {children} ); }; export const useExam = (): ExamContextType => { const context = useContext(ExamContext); if (context === undefined) { throw new Error("useExam must be used within an ExamProvider"); } return context; }; // Hook for exam results (only when exam is completed) - now returns null instead of throwing export const useExamResults = (): ExamAttempt | null => { const { currentAttempt, isExamCompleted, isHydrated } = useExam(); // Wait for hydration before making decisions if (!isHydrated) { return null; } // If no completed exam is found, return null (let component handle redirect) if (!isExamCompleted() || !currentAttempt) { return null; } return currentAttempt; };