From 2579c89f7c8cd1ccf41b9b87e0d771538c92432f Mon Sep 17 00:00:00 2001 From: solomonadzape95 Date: Thu, 29 Jan 2026 01:03:10 +0100 Subject: [PATCH 1/5] feat(quiz): refactor quiz page and implement custom hook for quiz logic - Replaced local state management with a custom hook `useQuiz` for better state handling. - Updated quiz page to utilize Redux for state management, including question fetching and answer submission. - Enhanced error and loading states for improved user experience. - Added API integration for fetching daily quests and submitting answers. - Refactored quiz logic to streamline question navigation and answer selection. --- frontend/app/quiz/page.tsx | 155 +++++++++++++------ frontend/hooks/useQuiz.ts | 158 +++++++++++++++++++ frontend/lib/api/quizApi.ts | 140 +++++++++++++++++ frontend/lib/features/quiz/quizSlice.ts | 196 +++++++++++++++++++----- package-lock.json | 45 +++++- 5 files changed, 603 insertions(+), 91 deletions(-) create mode 100644 frontend/hooks/useQuiz.ts create mode 100644 frontend/lib/api/quizApi.ts diff --git a/frontend/app/quiz/page.tsx b/frontend/app/quiz/page.tsx index 1f78332..347bb9a 100644 --- a/frontend/app/quiz/page.tsx +++ b/frontend/app/quiz/page.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState, useRef, useEffect } from "react"; +import { useRef, useEffect } from "react"; import { Nunito } from "next/font/google"; -import { MOCK_QUIZ } from "@/lib/Quiz_data"; +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"; @@ -13,79 +14,133 @@ const nunito = Nunito({ }); export default function QuizPage() { - 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 actionBtnRef = useRef(null); - const question = MOCK_QUIZ[step]; + const { + questions, + currentQuestion, + currentQuestionIndex, + selectedAnswerId, + isLoading, + isSubmitting, + isFinished, + submissionResult, + error, + score, + correctAnswersCount, + selectAnswer, + submitAnswer, + goToNextQuestion, + } = useQuiz({ + autoFetch: true, + fetchParams: { type: "daily-quest" }, + }); - const handleSelectOption = (optionId: string) => { - if (isSubmitted) return; - setSelectedId(optionId); - }; + const quizState = useAppSelector((state) => state.quiz); + + const isSubmitted = submissionResult !== null; useEffect(() => { - if (selectedId && actionBtnRef.current) { + if (selectedAnswerId && actionBtnRef.current) { actionBtnRef.current.focus(); } - }, [selectedId]); + }, [selectedAnswerId]); - const handleAction = () => { + const handleAction = async () => { if (!isSubmitted) { - setIsSubmitted(true); - const selectedOption = question.options.find( - (opt) => opt.id === selectedId, - ); - if (selectedOption?.isCorrect) { - setScore((prev) => prev + 1); + try { + await submitAnswer(); + } catch (err) { + console.error("Failed to submit answer:", err); } } else { - if (step < MOCK_QUIZ.length - 1) { - setStep(step + 1); - setSelectedId(null); - setIsSubmitted(false); - } else { - setIsFinished(true); - } + goToNextQuestion(); } }; + // 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 ? ( alert("Points Claimed!")} /> ) : (

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

- {question.options.map((opt) => { - const isSelected = selectedId === opt.id; + {currentQuestion.options.map((optionText, index) => { + const isSelected = selectedAnswerId === optionText; let state: "default" | "red" | "green" | "teal" = "default"; - if (isSubmitted) { + if (isSubmitted && submissionResult) { if (isSelected) { - state = opt.isCorrect ? "green" : "red"; - } else if (opt.isCorrect) { - state = "green"; + 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"; } } else if (isSelected) { state = "teal"; @@ -93,11 +148,11 @@ export default function QuizPage() { return ( handleSelectOption(opt.id)} + disabled={isSubmitted || isSubmitting} + onSelect={() => selectAnswer(optionText)} /> ); })} @@ -106,15 +161,19 @@ export default function QuizPage() {
)} diff --git a/frontend/hooks/useQuiz.ts b/frontend/hooks/useQuiz.ts new file mode 100644 index 0000000..1296679 --- /dev/null +++ b/frontend/hooks/useQuiz.ts @@ -0,0 +1,158 @@ +"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 new file mode 100644 index 0000000..2e52c35 --- /dev/null +++ b/frontend/lib/api/quizApi.ts @@ -0,0 +1,140 @@ +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. + 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 43e75e0..bdb62ba 100644 --- a/frontend/lib/features/quiz/quizSlice.ts +++ b/frontend/lib/features/quiz/quizSlice.ts @@ -1,23 +1,43 @@ -import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import { + fetchDailyQuest, + fetchPuzzles, + submitAnswer as submitAnswerApi, + type PuzzleResponseDto, + type SubmitAnswerRequestDto, +} from "@/lib/api/quizApi"; export interface Question { id: string; text: string; options: string[]; - correctAnswer: string; + correctAnswer?: string; // May be undefined until submission 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; - status: 'idle' | 'loading' | 'active' | 'submitting' | 'completed' | 'error'; + correctAnswersCount: number; + status: "idle" | "loading" | "active" | "submitting" | "completed" | "error"; error: string | null; } @@ -25,53 +45,111 @@ const initialState: QuizState = { questions: [], currentIndex: 0, answers: {}, + selectedAnswerId: null, + isSubmitting: false, + submissionResult: null, questionStartTime: null, totalSessionTime: 0, score: 0, - status: 'idle', + correctAnswersCount: 0, + 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 (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) + "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, + }; }, ); 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 @@ -85,41 +163,83 @@ 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.status = "active"; + state.selectedAnswerId = null; + state.submissionResult = null; 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.status = "loading"; + state.error = null; }) .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 || 'Error'; + 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"; }); }, }); -export const { startQuiz, submitAnswer, nextQuestion, exitQuiz } = - quizSlice.actions; +export const { + startQuiz, + submitAnswer, + nextQuestion, + exitQuiz, + selectAnswer, +} = quizSlice.actions; export default quizSlice.reducer; diff --git a/package-lock.json b/package-lock.json index 8d99953..262c41e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,6 +154,7 @@ "integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", @@ -200,6 +201,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" @@ -238,6 +240,7 @@ "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -346,6 +349,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -389,6 +393,7 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==", "license": "MIT", + "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^3.17.0", @@ -521,6 +526,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -835,6 +841,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -3522,6 +3529,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -3581,6 +3589,7 @@ "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3677,6 +3686,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", "integrity": "sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.1.0", @@ -4861,6 +4871,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5118,6 +5129,7 @@ "integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5259,6 +5271,7 @@ "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/types": "8.42.0", @@ -6254,6 +6267,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6339,6 +6353,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6787,6 +6802,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -7211,6 +7227,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -7676,13 +7693,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -8878,6 +8897,7 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8967,6 +8987,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9068,6 +9089,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9490,6 +9512,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -10984,6 +11007,7 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", "license": "MIT", + "peer": true, "dependencies": { "@ioredis/commands": "^1.3.0", "cluster-key-slot": "^1.1.0", @@ -11652,6 +11676,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -14488,6 +14513,7 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -14663,6 +14689,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -14989,6 +15016,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -15261,6 +15289,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -15270,6 +15299,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -15289,6 +15319,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -15360,7 +15391,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -15375,7 +15407,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -15699,6 +15732,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -15816,6 +15850,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -17430,6 +17465,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17825,6 +17861,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18325,7 +18362,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -18340,7 +18376,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } From 96024c81e8150de6a1cc348c5e13fc5f1b71a67d Mon Sep 17 00:00:00 2001 From: solomonadzape95 Date: Thu, 29 Jan 2026 01:14:26 +0100 Subject: [PATCH 2/5] refactor: remove unnecessary eslint-disable comments and improve error handling in auth and quiz components - Removed eslint-disable comments from various files for cleaner code. - Updated error handling in `CheckEmail`, `ForgotPassword`, and `ResetPassword` components to use a generic catch variable. - Adjusted import paths in quiz-related files for consistency. - Enhanced type definitions in quiz API response handling. --- .../src/auth/providers/sign-in.provider.ts | 1 - backend/src/redis/redis.constants.ts | 1 - backend/src/redis/redis.module.ts | 1 - backend/src/redis/redis.provider.ts | 1 - frontend/app/auth/check-email/page.tsx | 7 ++-- frontend/app/auth/forgot-password/page.tsx | 38 +++++++++++------- .../app/auth/reset-password/[token]/page.tsx | 39 +++++++++++-------- frontend/app/quiz/page.tsx | 10 ++--- frontend/hooks/useQuiz.ts | 6 +-- frontend/lib/api/quizApi.ts | 10 ++++- frontend/lib/features/quiz/quizSlice.ts | 2 +- 11 files changed, 68 insertions(+), 48 deletions(-) diff --git a/backend/src/auth/providers/sign-in.provider.ts b/backend/src/auth/providers/sign-in.provider.ts index 703b5ac..e76ed4c 100644 --- a/backend/src/auth/providers/sign-in.provider.ts +++ b/backend/src/auth/providers/sign-in.provider.ts @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ import { forwardRef, Inject, diff --git a/backend/src/redis/redis.constants.ts b/backend/src/redis/redis.constants.ts index 0905d76..6fddc65 100644 --- a/backend/src/redis/redis.constants.ts +++ b/backend/src/redis/redis.constants.ts @@ -1,2 +1 @@ -/* eslint-disable prettier/prettier */ export const REDIS_CLIENT = 'REDIS_CLIENT'; diff --git a/backend/src/redis/redis.module.ts b/backend/src/redis/redis.module.ts index bc3d021..d6e8f2e 100644 --- a/backend/src/redis/redis.module.ts +++ b/backend/src/redis/redis.module.ts @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ import { Global, Module, OnModuleDestroy, Inject } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { redisProvider } from './redis.provider'; diff --git a/backend/src/redis/redis.provider.ts b/backend/src/redis/redis.provider.ts index 1857e13..91fb8ed 100644 --- a/backend/src/redis/redis.provider.ts +++ b/backend/src/redis/redis.provider.ts @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ import { Provider } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Redis from 'ioredis'; diff --git a/frontend/app/auth/check-email/page.tsx b/frontend/app/auth/check-email/page.tsx index 159aa72..bceef9b 100644 --- a/frontend/app/auth/check-email/page.tsx +++ b/frontend/app/auth/check-email/page.tsx @@ -1,12 +1,10 @@ "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(); @@ -40,7 +38,8 @@ const CheckEmail = () => { } else { showError('Error', data.message || 'Failed to resend password reset link.'); } - } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { 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 c6e02c2..55c32e3 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, showWarning, showInfo } = useToast(); + const { showSuccess, showError } = useToast(); const [formData, setFormData] = useState({ email: '', }); @@ -52,25 +52,35 @@ const ForgotPassword = () => { } try { - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/forgot-password`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + 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 }), }, - 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.", + ); } - } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { 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 583b756..4d94930 100644 --- a/frontend/app/auth/reset-password/[token]/page.tsx +++ b/frontend/app/auth/reset-password/[token]/page.tsx @@ -1,8 +1,7 @@ "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"; @@ -76,31 +75,39 @@ const ResetPassword = () => { } try { - const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/reset-password/${token}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + 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, + }), }, - 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.", + ); } - } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { 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 347bb9a..ef41241 100644 --- a/frontend/app/quiz/page.tsx +++ b/frontend/app/quiz/page.tsx @@ -1,11 +1,11 @@ "use client"; import { 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 { 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"; const nunito = Nunito({ subsets: ["latin"], diff --git a/frontend/hooks/useQuiz.ts b/frontend/hooks/useQuiz.ts index 1296679..9bb21fe 100644 --- a/frontend/hooks/useQuiz.ts +++ b/frontend/hooks/useQuiz.ts @@ -1,7 +1,7 @@ "use client"; import { useEffect, useCallback } from "react"; -import { useAppDispatch, useAppSelector } from "@/lib/reduxHooks"; +import { useAppDispatch, useAppSelector } from "../lib/reduxHooks"; import { fetchQuestions, submitAnswerThunk, @@ -9,8 +9,8 @@ import { nextQuestion, startQuiz, type FetchQuestionsParams, -} from "@/lib/features/quiz/quizSlice"; -import type { Question } from "@/lib/features/quiz/quizSlice"; +} from "../lib/features/quiz/quizSlice"; +import type { Question } from "../lib/features/quiz/quizSlice"; function getUserIdFromToken(): string | null { if (typeof window === "undefined") { diff --git a/frontend/lib/api/quizApi.ts b/frontend/lib/api/quizApi.ts index 2e52c35..57b0297 100644 --- a/frontend/lib/api/quizApi.ts +++ b/frontend/lib/api/quizApi.ts @@ -124,7 +124,15 @@ export async function submitAnswer( }); // The backend likely returns a ProgressCalculationResult with validation info. - const raw = await handleResponse(response); + interface RawResponse { + validation?: { + isCorrect: boolean; + pointsEarned: number; + }; + isCorrect?: boolean; + pointsEarned?: number; + } + const raw = await handleResponse(response); const validation = raw?.validation ?? raw; diff --git a/frontend/lib/features/quiz/quizSlice.ts b/frontend/lib/features/quiz/quizSlice.ts index bdb62ba..c7e44dd 100644 --- a/frontend/lib/features/quiz/quizSlice.ts +++ b/frontend/lib/features/quiz/quizSlice.ts @@ -5,7 +5,7 @@ import { submitAnswer as submitAnswerApi, type PuzzleResponseDto, type SubmitAnswerRequestDto, -} from "@/lib/api/quizApi"; +} from "../../api/quizApi"; export interface Question { id: string; From cdbfb9fafbf0e81f5b43098943d44c402662c547 Mon Sep 17 00:00:00 2001 From: solomonadzape95 Date: Thu, 29 Jan 2026 08:07:33 +0100 Subject: [PATCH 3/5] feat(quiz): enhance submission feedback on quiz page - Updated the submission result display to show whether the answer was correct or incorrect, along with points earned. - Added a note regarding the backend's limitation in returning the correct option, emphasizing the highlighting of the selected answer only. --- frontend/app/quiz/page.tsx | 39 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/frontend/app/quiz/page.tsx b/frontend/app/quiz/page.tsx index 912c13d..b1184d6 100644 --- a/frontend/app/quiz/page.tsx +++ b/frontend/app/quiz/page.tsx @@ -160,31 +160,20 @@ export default function QuizPage() { ); })}
- {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} -

- )} - - ); - })()} + {isSubmitted && submissionResult && ( +
+
+ {submissionResult.isCorrect + ? `Correct! +${submissionResult.pointsEarned} pts` + : "Incorrect"} +
+

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

)} From ad8b88ac6ceaed5bd68dd9d0575934ce1d24f6e7 Mon Sep 17 00:00:00 2001 From: phertyameen Date: Thu, 29 Jan 2026 15:58:55 +0100 Subject: [PATCH 4/5] reorder puzzle routes --- backend/http/endpoint.http | 4 +--- .../puzzles/controllers/puzzles.controller.ts | 22 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/backend/http/endpoint.http b/backend/http/endpoint.http index 0b124a0..8cf3fa6 100644 --- a/backend/http/endpoint.http +++ b/backend/http/endpoint.http @@ -9,7 +9,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsI "password": "FatimaAminu@123" } - POST http://localhost:3000/auth/signIn Content-Type: application/json @@ -27,8 +26,7 @@ Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsI } -GET http://localhost:3000/daily-quest -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoiYW1pbnVmYXRpbWFAZ21haWwuY29tIiwiaWF0IjoxNzY5MzI2NTA3LCJleHAiOjE3NjkzMzAxMDcsImF1ZCI6ImxvY2FsaG9zdDozMDAwIiwiaXNzIjoibG9jYWxob3N0OjMwMDAifQ.YnrXEo1yns77DQgzHR0XO8m5MfTxT_ic9U_2je9nB6M +GET http://localhost:3000/puzzles/daily-quest diff --git a/backend/src/puzzles/controllers/puzzles.controller.ts b/backend/src/puzzles/controllers/puzzles.controller.ts index 8aee050..9d7461f 100644 --- a/backend/src/puzzles/controllers/puzzles.controller.ts +++ b/backend/src/puzzles/controllers/puzzles.controller.ts @@ -29,17 +29,6 @@ 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, @@ -60,4 +49,15 @@ 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 From 83b4b3a0cd1f2b75b33aa7fc493ad119bdb2d385 Mon Sep 17 00:00:00 2001 From: phertyameen Date: Thu, 29 Jan 2026 17:53:07 +0100 Subject: [PATCH 5/5] shaping frontend response --- backend/http/endpoint.http | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/http/endpoint.http b/backend/http/endpoint.http index 8cf3fa6..221bf63 100644 --- a/backend/http/endpoint.http +++ b/backend/http/endpoint.http @@ -28,6 +28,24 @@ 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 +