diff --git a/backend/http/endpoint.http b/backend/http/endpoint.http index 221bf63..0b124a0 100644 --- a/backend/http/endpoint.http +++ b/backend/http/endpoint.http @@ -9,6 +9,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsI "password": "FatimaAminu@123" } + POST http://localhost:3000/auth/signIn Content-Type: application/json @@ -26,25 +27,8 @@ Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsI } -GET http://localhost:3000/puzzles/daily-quest - -### Get all puzzles -GET http://localhost:3000/puzzles - -### Get puzzles by category -GET http://localhost:3000/puzzles?categoryId=397b1a88-3a41-4c14-afee-20f57554368b - -### Get puzzles by difficulty -GET http://localhost:3000/puzzles?difficulty=INTERMEDIATE - -### Get puzzles by category AND difficulty -GET http://localhost:3000/puzzles?categoryId=397b1a88-3a41-4c14-afee-20f57554368b&difficulty=INTERMEDIATE - -### Get puzzles with pagination -GET http://localhost:3000/puzzles?page=1&limit=10 - -### Get puzzles with all filters -GET http://localhost:3000/puzzles?categoryId=397b1a88-3a41-4c14-afee-20f57554368b&difficulty=INTERMEDIATE&page=1&limit=10 +GET http://localhost:3000/daily-quest +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiYW1pbnVmYXRpbWFAZ21haWwuY29tIiwiaWF0IjoxNzY5MzI2NTA3LCJleHAiOjE3NjkzMzAxMDcsImF1ZCI6ImxvY2FsaG9zdDozMDAwIiwiaXNzIjoibG9jYWxob3N0OjMwMDAifQ.YnrXEo1yns77DQgzHR0XO8m5MfTxT_ic9U_2je9nB6M diff --git a/backend/src/puzzles/controllers/puzzles.controller.ts b/backend/src/puzzles/controllers/puzzles.controller.ts index 9d7461f..8aee050 100644 --- a/backend/src/puzzles/controllers/puzzles.controller.ts +++ b/backend/src/puzzles/controllers/puzzles.controller.ts @@ -29,6 +29,17 @@ export class PuzzlesController { return this.puzzlesService.create(createPuzzleDto); } + @ApiOperation({ summary: 'Get a puzzle by ID' }) + @ApiResponse({ + status: 200, + description: 'Puzzle retrieved successfully', + type: Puzzle, + }) + @Get(':id') + getById(@Param('id') id: string) { + return this.puzzlesService.getPuzzleById(id); + } + @ApiOperation({ summary: 'Get daily quest puzzles' }) @ApiResponse({ status: 200, @@ -49,15 +60,4 @@ export class PuzzlesController { findAll(@Query() query: PuzzleQueryDto) { return this.puzzlesService.findAll(query); } - - @ApiOperation({ summary: 'Get a puzzle by ID' }) - @ApiResponse({ - status: 200, - description: 'Puzzle retrieved successfully', - type: Puzzle, - }) - @Get(':id') - getById(@Param('id') id: string) { - return this.puzzlesService.getPuzzleById(id); - } } \ No newline at end of file diff --git a/frontend/app/auth/check-email/page.tsx b/frontend/app/auth/check-email/page.tsx index bceef9b..159aa72 100644 --- a/frontend/app/auth/check-email/page.tsx +++ b/frontend/app/auth/check-email/page.tsx @@ -1,10 +1,12 @@ "use client"; import ErrorBoundary from "@/components/ErrorBoundary"; +import Link from "next/link"; +import Image from 'next/image'; import { useToast } from "@/components/ui/ToastProvider"; import { useState } from "react"; import Button from "@/components/ui/Button"; - +import { Mail } from "lucide-react"; const CheckEmail = () => { const { showSuccess, showError } = useToast(); @@ -38,8 +40,7 @@ const CheckEmail = () => { } else { showError('Error', data.message || 'Failed to resend password reset link.'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { + } catch (error) { showError('Error', 'An unexpected error occurred. Please try again later.'); } finally { setIsLoading(false); diff --git a/frontend/app/auth/forgot-password/page.tsx b/frontend/app/auth/forgot-password/page.tsx index 55c32e3..c6e02c2 100644 --- a/frontend/app/auth/forgot-password/page.tsx +++ b/frontend/app/auth/forgot-password/page.tsx @@ -12,7 +12,7 @@ import Button from "@/components/ui/Button"; const ForgotPassword = () => { const router = useRouter(); - const { showSuccess, showError } = useToast(); + const { showSuccess, showError, showWarning, showInfo } = useToast(); const [formData, setFormData] = useState({ email: '', }); @@ -52,35 +52,25 @@ const ForgotPassword = () => { } try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/auth/forgot-password`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email: formData.email }), + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/forgot-password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - ); - + body: JSON.stringify({ email: formData.email }), + }); + const data = await response.json(); - + if (response.ok) { // Store email for resend functionality - sessionStorage.setItem("resetEmail", formData.email); - showSuccess( - "Success", - data.message || "Password reset link sent to your email.", - ); - router.push("/auth/check-email"); + sessionStorage.setItem('resetEmail', formData.email); + showSuccess('Success', data.message || 'Password reset link sent to your email.'); + router.push('/auth/check-email'); } else { - showError( - "Error", - data.message || "Failed to send password reset link.", - ); + showError('Error', data.message || 'Failed to send password reset link.'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { + } catch (error) { showError('Error', 'An unexpected error occurred. Please try again later.'); } finally { setIsLoading(false); diff --git a/frontend/app/auth/reset-password/[token]/page.tsx b/frontend/app/auth/reset-password/[token]/page.tsx index 4d94930..583b756 100644 --- a/frontend/app/auth/reset-password/[token]/page.tsx +++ b/frontend/app/auth/reset-password/[token]/page.tsx @@ -1,7 +1,8 @@ "use client"; import ErrorBoundary from "@/components/ErrorBoundary"; - +import Link from "next/link"; +import Image from 'next/image'; import { useRouter, useParams } from "next/navigation"; import Input from "@/components/ui/Input"; import { useToast } from "@/components/ui/ToastProvider"; @@ -75,39 +76,31 @@ const ResetPassword = () => { } try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/auth/reset-password/${token}`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - password: formData.password, - }), + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/reset-password/${token}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - ); + body: JSON.stringify({ + password: formData.password, + }), + }); const data = await response.json(); if (response.ok) { - showSuccess("Success", data.message || "Password reset successfully."); + showSuccess('Success', data.message || 'Password reset successfully.'); // Clear the stored email - sessionStorage.removeItem("resetEmail"); - localStorage.removeItem("resetEmail"); + sessionStorage.removeItem('resetEmail'); + localStorage.removeItem('resetEmail'); // Redirect to sign in after 1.5 seconds setTimeout(() => { - router.push("/auth/signin"); + router.push('/auth/signin'); }, 1500); } else { - showError( - "Error", - data.message || - "Failed to reset password. The link may have expired.", - ); + showError('Error', data.message || 'Failed to reset password. The link may have expired.'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { + } catch (error) { showError('Error', 'An unexpected error occurred. Please try again later.'); } finally { setIsLoading(false); diff --git a/frontend/app/quiz/page.tsx b/frontend/app/quiz/page.tsx index b1184d6..20d6e78 100644 --- a/frontend/app/quiz/page.tsx +++ b/frontend/app/quiz/page.tsx @@ -1,11 +1,10 @@ "use client"; -import { useRef, useEffect } from "react"; +import { useState, useRef, useEffect } from "react"; import { Nunito } from "next/font/google"; -import { useQuiz } from "../../hooks/useQuiz"; -import { useAppSelector } from "../../lib/reduxHooks"; -import { QuizHeader } from "../../components/quiz/QuizHeader"; -import { AnswerOption } from "../../components/quiz/AnswerOption"; -import { LevelComplete } from "../../components/quiz/LevelComplete"; +import { MOCK_QUIZ } from "@/lib/Quiz_data"; +import { QuizHeader } from "@/components/quiz/QuizHeader"; +import { AnswerOption } from "@/components/quiz/AnswerOption"; +import { LevelComplete } from "@/components/quiz/LevelComplete"; const nunito = Nunito({ subsets: ["latin"], @@ -14,136 +13,83 @@ const nunito = Nunito({ }); export default function QuizPage() { - const actionBtnRef = useRef(null); + const [step, setStep] = useState(0); + const [selectedId, setSelectedId] = useState(null); + const [isSubmitted, setIsSubmitted] = useState(false); + const [score, setScore] = useState(0); + const [isFinished, setIsFinished] = useState(false); - const { - questions, - currentQuestion, - currentQuestionIndex, - selectedAnswerId, - isLoading, - isSubmitting, - isFinished, - submissionResult, - error, - score, - correctAnswersCount, - selectAnswer, - submitAnswer, - goToNextQuestion, - } = useQuiz({ - autoFetch: true, - fetchParams: { type: "daily-quest" }, - }); + const actionBtnRef = useRef(null); - const quizState = useAppSelector((state) => state.quiz); + const QUIZ = MOCK_QUIZ.slice(0, 2); + const question = QUIZ[step]; - const isSubmitted = submissionResult !== null; + const handleSelectOption = (optionId: string) => { + if (isSubmitted) return; + setSelectedId(optionId); + }; useEffect(() => { - if (selectedAnswerId && actionBtnRef.current) { + if (selectedId && actionBtnRef.current) { actionBtnRef.current.focus(); } - }, [selectedAnswerId]); + }, [selectedId]); - const handleAction = async () => { + const handleAction = () => { if (!isSubmitted) { - try { - await submitAnswer(); - } catch (err) { - console.error("Failed to submit answer:", err); - } + setTimeout(() => { + setIsSubmitted(true); + const selectedOption = question.options.find( + (opt) => opt.id === selectedId, + ); + if (selectedOption?.isCorrect) { + setScore((prev) => prev + 1); + } + }, 150); } else { - goToNextQuestion(); + if (step < QUIZ.length - 1) { + setStep(step + 1); + setSelectedId(null); + setIsSubmitted(false); + } else { + setIsFinished(true); + } } }; - // Format time taken from total session time - const formatTimeTaken = () => { - const totalSeconds = Math.floor(quizState.totalSessionTime / 1000); - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - return `${minutes}:${seconds.toString().padStart(2, "0")}`; - }; - - // Loading state - if (isLoading) { - return ( -
-
Loading questions...
-
- ); - } - - // Error state - if (error && questions.length === 0) { - return ( -
-
Error: {error}
-
- ); - } - - // No questions state - if (!currentQuestion && !isLoading) { - return ( -
-
No questions available
-
- ); - } - - if (!currentQuestion) { - return null; - } - return (
- {!isFinished && ( - - )} + {!isFinished && }
{isFinished ? ( alert("Points Claimed!")} /> ) : (

- {currentQuestion.text} + {question.question}

Points: {score * 10}

- {currentQuestion.options.map((optionText, index) => { - const isSelected = selectedAnswerId === optionText; + {question.options.map((opt) => { + const isSelected = selectedId === opt.id; let state: "default" | "red" | "green" | "teal" = "default"; - if (isSubmitted && submissionResult) { + if (isSubmitted) { if (isSelected) { - state = submissionResult.isCorrect ? "green" : "red"; - } else if (submissionResult.isCorrect) { - // Show correct answer in green even if not selected - // Note: We don't know which option is correct from backend - // This would need backend to return correctAnswer in response - state = "default"; + state = opt.isCorrect ? "teal" : "red"; + } else if (opt.isCorrect) { + state = "teal"; } } else if (isSelected) { state = "teal"; @@ -151,48 +97,55 @@ export default function QuizPage() { return ( selectAnswer(optionText)} + disabled={isSubmitted} + onSelect={() => handleSelectOption(opt.id)} /> ); })}
- {isSubmitted && submissionResult && ( -
-
- {submissionResult.isCorrect - ? `Correct! +${submissionResult.pointsEarned} pts` - : "Incorrect"} -
-

- Note: backend does not return the correct option, so we only highlight your selected answer. -

+ {isSubmitted && ( +
+ {(() => { + const selectedOption = question.options.find( + (o) => o.id === selectedId, + ); + const correctOption = question.options.find( + (o) => o.isCorrect, + ); + const wasCorrect = !!selectedOption?.isCorrect; + return ( + <> + {!wasCorrect && correctOption && ( +
+ Correct answer: {correctOption.text} +
+ )} + {question.explanation && ( +

+ {question.explanation} +

+ )} + + ); + })()}
)}
)} diff --git a/frontend/hooks/useQuiz.ts b/frontend/hooks/useQuiz.ts deleted file mode 100644 index 9bb21fe..0000000 --- a/frontend/hooks/useQuiz.ts +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import { useEffect, useCallback } from "react"; -import { useAppDispatch, useAppSelector } from "../lib/reduxHooks"; -import { - fetchQuestions, - submitAnswerThunk, - selectAnswer, - nextQuestion, - startQuiz, - type FetchQuestionsParams, -} from "../lib/features/quiz/quizSlice"; -import type { Question } from "../lib/features/quiz/quizSlice"; - -function getUserIdFromToken(): string | null { - if (typeof window === "undefined") { - return null; - } - - const token = localStorage.getItem("accessToken"); - if (!token) { - return null; - } - - try { - // Decode JWT token (base64url decode the payload) - const parts = token.split("."); - if (parts.length !== 3) { - return null; - } - - const payload = parts[1]; - const decoded = JSON.parse( - atob(payload.replace(/-/g, "+").replace(/_/g, "/")), - ); - return decoded.sub || null; - } catch (error) { - console.error("Failed to decode token:", error); - return null; - } -} - -export interface UseQuizOptions { - autoFetch?: boolean; - fetchParams?: FetchQuestionsParams; -} - -export function useQuiz(options: UseQuizOptions = {}) { - const { autoFetch = false, fetchParams = { type: "daily-quest" } } = - options; - - const dispatch = useAppDispatch(); - const quizState = useAppSelector((state) => state.quiz); - - const { - questions, - currentIndex, - selectedAnswerId, - isSubmitting, - submissionResult, - status, - error, - questionStartTime, - score, - correctAnswersCount, - } = quizState; - - const currentQuestion: Question | null = - questions.length > 0 && currentIndex < questions.length - ? questions[currentIndex] - : null; - - const isLoading = status === "loading"; - const isFinished = status === "completed"; - - // Auto-fetch questions on mount if requested - useEffect(() => { - if (autoFetch && questions.length === 0 && status === "idle") { - dispatch(fetchQuestions(fetchParams)); - } - }, [autoFetch, questions.length, status, fetchParams, dispatch]); - - // Start quiz when questions are loaded - useEffect(() => { - if ( - questions.length > 0 && - status === "idle" && - questionStartTime === null - ) { - dispatch(startQuiz()); - } - }, [questions.length, status, questionStartTime, dispatch]); - - const selectAnswerHandler = useCallback( - (answerId: string) => { - if (status !== "active" || isSubmitting) { - return; - } - dispatch(selectAnswer(answerId)); - }, - [dispatch, status, isSubmitting], - ); - - const submitAnswerHandler = useCallback(async () => { - if (!currentQuestion || !selectedAnswerId || isSubmitting) { - return; - } - - const userId = getUserIdFromToken(); - if (!userId) { - throw new Error("User not authenticated"); - } - - // Calculate time spent - const timeSpent = questionStartTime - ? Math.floor((Date.now() - questionStartTime) / 1000) - : 0; - - await dispatch( - submitAnswerThunk({ - userId, - puzzleId: currentQuestion.id, - categoryId: currentQuestion.categoryId, - userAnswer: selectedAnswerId, - timeSpent, - }), - ).unwrap(); - }, [ - currentQuestion, - selectedAnswerId, - isSubmitting, - questionStartTime, - dispatch, - ]); - - const goToNextQuestion = useCallback(() => { - if (status === "submitting" || status === "active") { - dispatch(nextQuestion()); - } - }, [dispatch, status]); - - return { - questions, - currentQuestionIndex: currentIndex, - currentQuestion, - selectedAnswerId, - isLoading, - isSubmitting, - error, - isFinished, - submissionResult, - score, - correctAnswersCount, - selectAnswer: selectAnswerHandler, - submitAnswer: submitAnswerHandler, - goToNextQuestion, - }; -} diff --git a/frontend/lib/api/quizApi.ts b/frontend/lib/api/quizApi.ts deleted file mode 100644 index 57b0297..0000000 --- a/frontend/lib/api/quizApi.ts +++ /dev/null @@ -1,148 +0,0 @@ -export interface PuzzleResponseDto { - id: string; - question: string; - options: string[]; - difficulty: string; - categoryId: string; - points: number; - timeLimit: number; - isCompleted: boolean; -} - -export interface DailyQuestResponseDto { - id: number; - questDate: string; - totalQuestions: number; - completedQuestions: number; - isCompleted: boolean; - pointsEarned: number; - createdAt: string; - completedAt?: string | null; - puzzles: PuzzleResponseDto[]; -} - -export interface SubmitAnswerRequestDto { - userId: string; - puzzleId: string; - categoryId: string; - userAnswer: string; - timeSpent: number; -} - -export interface SubmitAnswerResponseDto { - isCorrect: boolean; - pointsEarned: number; -} - -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? ""; - -function getAuthHeaders(): Record { - if (typeof window === "undefined") { - return {}; - } - - const token = window.localStorage.getItem("accessToken"); - - if (!token) { - return {}; - } - - return { - Authorization: `Bearer ${token}`, - }; -} - -async function handleResponse(response: Response): Promise { - const contentType = response.headers.get("Content-Type"); - const isJson = contentType && contentType.includes("application/json"); - - const data = isJson ? await response.json() : null; - - if (!response.ok) { - const message = - (data && (data.message as string | undefined)) || - `Request failed with status ${response.status}`; - throw new Error(message); - } - - return data as T; -} - -export async function fetchDailyQuest(): Promise { - const headers: HeadersInit = { - "Content-Type": "application/json", - ...getAuthHeaders(), - }; - const response = await fetch(`${API_BASE_URL}/puzzles/daily-quest`, { - method: "GET", - headers, - }); - - return handleResponse(response); -} - -export interface FetchPuzzlesParams { - categoryId: string; - difficulty?: string; -} - -export async function fetchPuzzles( - params: FetchPuzzlesParams, -): Promise { - const searchParams = new URLSearchParams(); - searchParams.set("categoryId", params.categoryId); - if (params.difficulty) { - searchParams.set("difficulty", params.difficulty); - } - - const headers: HeadersInit = { - "Content-Type": "application/json", - ...getAuthHeaders(), - }; - const response = await fetch( - `${API_BASE_URL}/puzzles?${searchParams.toString()}`, - { - method: "GET", - headers, - }, - ); - - return handleResponse(response); -} - -export async function submitAnswer( - payload: SubmitAnswerRequestDto, -): Promise { - const headers: HeadersInit = { - "Content-Type": "application/json", - ...getAuthHeaders(), - }; - const response = await fetch(`${API_BASE_URL}/puzzles/submit`, { - method: "POST", - headers, - body: JSON.stringify(payload), - }); - - // The backend likely returns a ProgressCalculationResult with validation info. - interface RawResponse { - validation?: { - isCorrect: boolean; - pointsEarned: number; - }; - isCorrect?: boolean; - pointsEarned?: number; - } - const raw = await handleResponse(response); - - const validation = raw?.validation ?? raw; - - return { - isCorrect: - typeof validation?.isCorrect === "boolean" ? validation.isCorrect : false, - pointsEarned: - typeof validation?.pointsEarned === "number" - ? validation.pointsEarned - : 0, - }; -} - diff --git a/frontend/lib/features/quiz/quizSlice.ts b/frontend/lib/features/quiz/quizSlice.ts index c7e44dd..43e75e0 100644 --- a/frontend/lib/features/quiz/quizSlice.ts +++ b/frontend/lib/features/quiz/quizSlice.ts @@ -1,43 +1,23 @@ -import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; -import { - fetchDailyQuest, - fetchPuzzles, - submitAnswer as submitAnswerApi, - type PuzzleResponseDto, - type SubmitAnswerRequestDto, -} from "../../api/quizApi"; +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; export interface Question { id: string; text: string; options: string[]; - correctAnswer?: string; // May be undefined until submission + correctAnswer: string; points: number; - categoryId: string; - difficulty?: string; - timeLimit?: number; - isCompleted?: boolean; -} - -interface SubmissionResult { - isCorrect: boolean; - pointsEarned: number; } interface QuizState { questions: Question[]; currentIndex: number; answers: Record; - selectedAnswerId: string | null; - isSubmitting: boolean; - submissionResult: SubmissionResult | null; questionStartTime: number | null; totalSessionTime: number; score: number; - correctAnswersCount: number; - status: "idle" | "loading" | "active" | "submitting" | "completed" | "error"; + status: 'idle' | 'loading' | 'active' | 'submitting' | 'completed' | 'error'; error: string | null; } @@ -45,111 +25,53 @@ const initialState: QuizState = { questions: [], currentIndex: 0, answers: {}, - selectedAnswerId: null, - isSubmitting: false, - submissionResult: null, questionStartTime: null, totalSessionTime: 0, score: 0, - correctAnswersCount: 0, - status: "idle", + status: 'idle', error: null, }; -export type FetchQuestionsParams = - | { type: "daily-quest" } - | { type: "category"; categoryId: string; difficulty?: string }; - -function mapPuzzleToQuestion(puzzle: PuzzleResponseDto): Question { - return { - id: puzzle.id, - text: puzzle.question, - options: puzzle.options, - points: puzzle.points, - categoryId: puzzle.categoryId, - difficulty: puzzle.difficulty, - timeLimit: puzzle.timeLimit, - isCompleted: puzzle.isCompleted, - // correctAnswer will be set after submission - }; -} - export const fetchQuestions = createAsyncThunk( - "quiz/fetchQuestions", - async (params: FetchQuestionsParams) => { - let puzzles: PuzzleResponseDto[]; - - if (params.type === "daily-quest") { - const quest = await fetchDailyQuest(); - puzzles = quest.puzzles; - } else { - puzzles = await fetchPuzzles({ - categoryId: params.categoryId, - difficulty: params.difficulty, - }); - } - - return puzzles.map(mapPuzzleToQuestion); - }, -); - -export const submitAnswerThunk = createAsyncThunk( - "quiz/submitAnswer", - async ( - payload: { - userId: string; - puzzleId: string; - categoryId: string; - userAnswer: string; - timeSpent: number; - }, - { getState }, - ) => { - const state = getState() as { quiz: QuizState }; - const currentQuestion = state.quiz.questions[state.quiz.currentIndex]; - - if (!currentQuestion) { - throw new Error("No current question"); - } - - const submitPayload: SubmitAnswerRequestDto = { - userId: payload.userId, - puzzleId: payload.puzzleId, - categoryId: payload.categoryId, - userAnswer: payload.userAnswer, - timeSpent: payload.timeSpent, - }; - - const result = await submitAnswerApi(submitPayload); - - return { - ...result, - puzzleId: payload.puzzleId, - }; + 'quiz/fetchQuestions', + async (category: string) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return [ + { + id: 'q1', + text: 'What is 2+2?', + options: ['3', '4', '5'], + correctAnswer: '4', + points: 10, + }, + { + id: 'q2', + text: 'What is Next.js?', + options: ['DB', 'Framework', 'Language'], + correctAnswer: 'Framework', + points: 20, + }, + ] as Question[]; + console.log(category) }, ); const quizSlice = createSlice({ - name: "quiz", + name: 'quiz', initialState, reducers: { startQuiz: (state) => { - state.status = "active"; + state.status = 'active'; state.currentIndex = 0; state.score = 0; - state.correctAnswersCount = 0; state.totalSessionTime = 0; state.answers = {}; - state.selectedAnswerId = null; - state.submissionResult = null; state.questionStartTime = Date.now(); }, - selectAnswer: (state, action: PayloadAction) => { - state.selectedAnswerId = action.payload; - }, submitAnswer: (state, action: PayloadAction) => { const currentQ = state.questions[state.currentIndex]; - if (!currentQ || state.status !== "active") return; + if (!currentQ || state.status !== 'active') return; const now = Date.now(); const duration = state.questionStartTime @@ -163,83 +85,41 @@ const quizSlice = createSlice({ state.score += currentQ.points; } - state.status = "submitting"; + state.status = 'submitting'; state.questionStartTime = null; }, nextQuestion: (state) => { if (state.currentIndex < state.questions.length - 1) { state.currentIndex++; - state.status = "active"; - state.selectedAnswerId = null; - state.submissionResult = null; + state.status = 'active'; state.questionStartTime = Date.now(); } else { - state.status = "completed"; + state.status = 'completed'; state.questionStartTime = null; } }, exitQuiz: (state) => { - state.status = "idle"; + state.status = 'idle'; state.questions = []; state.answers = {}; - state.selectedAnswerId = null; - state.submissionResult = null; }, }, extraReducers: (builder) => { builder .addCase(fetchQuestions.pending, (state) => { - state.status = "loading"; - state.error = null; + state.status = 'loading'; }) .addCase(fetchQuestions.fulfilled, (state, action) => { - state.status = "idle"; + state.status = 'idle'; state.questions = action.payload; - state.currentIndex = 0; - state.error = null; }) .addCase(fetchQuestions.rejected, (state, action) => { - state.status = "error"; - state.error = action.error.message || "Failed to fetch questions"; - }) - .addCase(submitAnswerThunk.pending, (state) => { - state.isSubmitting = true; - state.error = null; - }) - .addCase(submitAnswerThunk.fulfilled, (state, action) => { - state.isSubmitting = false; - state.submissionResult = { - isCorrect: action.payload.isCorrect, - pointsEarned: action.payload.pointsEarned, - }; - - // Update score and correct answers count if correct - if (action.payload.isCorrect) { - const currentQ = state.questions[state.currentIndex]; - if (currentQ) { - state.score += action.payload.pointsEarned; - state.correctAnswersCount += 1; - } - } - - // Store the answer - const currentQ = state.questions[state.currentIndex]; - if (currentQ && state.selectedAnswerId) { - state.answers[currentQ.id] = state.selectedAnswerId; - } - }) - .addCase(submitAnswerThunk.rejected, (state, action) => { - state.isSubmitting = false; - state.error = action.error.message || "Failed to submit answer"; + state.status = 'error'; + state.error = action.error.message || 'Error'; }); }, }); -export const { - startQuiz, - submitAnswer, - nextQuestion, - exitQuiz, - selectAnswer, -} = quizSlice.actions; +export const { startQuiz, submitAnswer, nextQuestion, exitQuiz } = + quizSlice.actions; export default quizSlice.reducer;