diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index df343c5..90d6636 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -58,7 +58,7 @@ export default function RegisterPage() { } try { await register(form, setToken); - router.push("/tabs/home"); + router.push("/home"); } catch (error: any) { console.error("Error:", error.response || error.message); if (error.response?.detail) { diff --git a/app/(tabs)/paper/page.tsx b/app/(tabs)/paper/page.tsx new file mode 100644 index 0000000..bbc1322 --- /dev/null +++ b/app/(tabs)/paper/page.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import Header from "@/components/Header"; +import DestructibleAlert from "@/components/DestructibleAlert"; +import BackgroundWrapper from "@/components/BackgroundWrapper"; +import { API_URL } from "@/lib/auth"; +import { Loader, RefreshCw } from "lucide-react"; + +interface Mock { + id: string; + title: string; + rating: number; +} + +// For App Router (Next.js 13+ with app directory) +export default function PaperScreen() { + const router = useRouter(); + const searchParams = useSearchParams(); + const name = searchParams.get("name") || ""; + + const [questions, setQuestions] = useState(null); + const [errorMsg, setErrorMsg] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const [componentKey, setComponentKey] = useState(0); + + async function fetchMocks() { + try { + const questionResponse = await fetch(`${API_URL}/mocks`, { + method: "GET", + }); + const fetchedQuestionData: Mock[] = await questionResponse.json(); + console.log(fetchedQuestionData[0]?.id); + setQuestions(fetchedQuestionData); + } catch (error) { + setErrorMsg(error instanceof Error ? error.message : "An error occurred"); + } + } + + useEffect(() => { + if (name) { + fetchMocks(); + } + }, [name]); + + const onRefresh = async () => { + setRefreshing(true); + + await fetchMocks(); + setComponentKey((prevKey) => prevKey + 1); + setTimeout(() => { + setRefreshing(false); + }, 1000); + }; + + if (errorMsg) { + return ( + +
+
+
+
+ +
+
+ +
+
+ {/* */} +
+
+ ); + } + + return ( + +
+
+
+
+ {questions ? ( + questions.map((mock) => ( +
+ +
+ )) + ) : ( +
+
+

Loading...

+
+ )} +
+
+ +
+
+
+ {/* */} +
+ ); +} diff --git a/app/(tabs)/unit/page.tsx b/app/(tabs)/unit/page.tsx index ef52205..8821005 100644 --- a/app/(tabs)/unit/page.tsx +++ b/app/(tabs)/unit/page.tsx @@ -33,7 +33,7 @@ const Unit = () => { />
-
+
{units ? ( units.map((unit) => ( + ))} +
+
+ ) +); + +QuestionItem.displayName = "QuestionItem"; + +const reducer = (state: AnswerState, action: AnswerAction): AnswerState => { + switch (action.type) { + case "SELECT_ANSWER": + return { ...state, [action.questionId]: action.option }; + default: + return state; + } }; -export default page; +export default function ExamPage() { + const router = useRouter(); + const params = useParams(); + const searchParams = useSearchParams(); + + const id = params.id as string; + const time = searchParams.get("time"); + + const { setInitialTime, stopTimer } = useTimer(); + + const [questions, setQuestions] = useState(null); + const [answers, dispatch] = useReducer(reducer, {}); + const [loading, setLoading] = useState(true); + const [submissionLoading, setSubmissionLoading] = useState(false); + + const fetchQuestions = async () => { + try { + const response = await fetch(`${API_URL}/mock/${id}`, { + method: "GET", + }); + const data = await response.json(); + setQuestions(data.questions); + } catch (error) { + console.error("Error fetching questions:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchQuestions(); + if (time) { + setInitialTime(Number(time)); + } + }, [id, time, setInitialTime]); + + const handleSelect = useCallback((questionId: number, option: string) => { + dispatch({ type: "SELECT_ANSWER", questionId, option }); + }, []); + + const handleSubmit = async () => { + stopTimer(); + setSubmissionLoading(true); + + const payload = { + mock_id: id, + data: answers, + }; + + try { + const response = await fetch(`${API_URL}/submit`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${await getToken()}`, + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error( + "Submission failed:", + errorData.message || "Unknown error" + ); + return; + } + + const responseData = await response.json(); + + router.push( + `/exam/results?id=${id}&answers=${encodeURIComponent( + JSON.stringify(responseData) + )}` + ); + } catch (error) { + console.error("Error submitting answers:", error); + } finally { + setSubmissionLoading(false); + } + }; + + const showExitDialog = () => { + if (window.confirm("Are you sure you want to quit the exam?")) { + stopTimer(); + router.push("/unit"); + } + }; + + // Handle browser back button + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + e.preventDefault(); + e.returnValue = ""; + return ""; + }; + + const handlePopState = (e: PopStateEvent) => { + e.preventDefault(); + showExitDialog(); + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + window.addEventListener("popstate", handlePopState); + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + window.removeEventListener("popstate", handlePopState); + }; + }, []); + + if (submissionLoading) { + return ( +
+
+
+
+

Submitting...

+
+
+
+ ); + } + + return ( +
+
+ {loading ? ( +
+
+
+ ) : ( +
+ {questions?.map((question) => ( + + ))} +
+ )} + +
+
+ +
+
+
+ + +
+ ); +} diff --git a/app/exam/pretest/page.tsx b/app/exam/pretest/page.tsx index a54c709..5813018 100644 --- a/app/exam/pretest/page.tsx +++ b/app/exam/pretest/page.tsx @@ -1,7 +1,190 @@ -import React from "react"; +"use client"; -const page = () => { - return
page
; -}; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { ArrowLeft, HelpCircle, Clock, XCircle } from "lucide-react"; +import DestructibleAlert from "@/components/DestructibleAlert"; +import BackgroundWrapper from "@/components/BackgroundWrapper"; +import { API_URL } from "@/lib/auth"; -export default page; +interface Metadata { + metadata: { + quantity: number; + type: string; + duration: number; + marking: string; + }; +} + +export default function PretestPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Get params from URL search params + const name = searchParams.get("name") || ""; + const id = searchParams.get("id") || ""; + const title = searchParams.get("title") || ""; + const rating = searchParams.get("rating") || ""; + + const [metadata, setMetadata] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + async function fetchQuestions() { + if (!id) return; + + try { + setLoading(true); + const questionResponse = await fetch(`${API_URL}/mock/${id}`, { + method: "GET", + }); + + if (!questionResponse.ok) { + throw new Error("Failed to fetch questions"); + } + + const fetchedMetadata: Metadata = await questionResponse.json(); + setMetadata(fetchedMetadata); + } catch (error) { + console.error(error); + setError(error instanceof Error ? error.message : "An error occurred"); + } finally { + setLoading(false); + } + } + + useEffect(() => { + if (id) { + fetchQuestions(); + } + }, [id]); + + if (error) { + return ( + +
+
+ + +
+ {/* */} +
+
+ ); + } + + return ( + +
+
+ {metadata ? ( +
+ + +

{title}

+ +

+ Rating: {rating} / 10 +

+ +
+
+ +
+

+ {metadata.metadata.quantity} +

+

+ {metadata.metadata.type} +

+
+
+ +
+ +
+

+ {metadata.metadata.duration} mins +

+

Time Taken

+
+
+ +
+ +
+

+ {metadata.metadata.marking} +

+

+ From each wrong answer +

+
+
+
+ +
+

Ready yourself!

+ +
+ +

+ You must complete this test in one session - make sure your + internet connection is reliable. +

+
+ +
+ +

+ There is negative marking for the wrong answer. +

+
+ +
+ +

+ The more you answer correctly, the better chance you have of + winning a badge. +

+
+ +
+ +

+ You can retake this test however many times you want. But, + you will earn points only once. +

+
+
+
+ ) : ( +
+
+

Loading...

+
+ )} +
+ + + + {/* */} +
+
+ ); +} diff --git a/app/exam/results/page.tsx b/app/exam/results/page.tsx index a54c709..32a50f4 100644 --- a/app/exam/results/page.tsx +++ b/app/exam/results/page.tsx @@ -1,7 +1,277 @@ -import React from "react"; +"use client"; -const page = () => { - return
page
; +import { useRouter, useSearchParams } from "next/navigation"; +import { ArrowLeft, LocateIcon } from "lucide-react"; +import { useEffect } from "react"; + +// Types +interface Question { + question: string; + options: Record; + correctAnswer: string; + userAnswer: string | null; + isCorrect: boolean; + solution: string; +} + +interface ResultSheet { + score: number; + questions: Question[]; +} + +const ResultsPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const answersParam = searchParams.get("answers"); + + if (!answersParam) { + return ( +
+
+

+ No results found +

+ +
+
+ ); + } + + const resultSheet: ResultSheet = JSON.parse(decodeURIComponent(answersParam)); + + const getScoreMessage = (score: number) => { + if (score < 30) return "Try harder!"; + if (score < 70) return "Getting Better"; + return "You did great!"; + }; + + const getStatusColor = (question: Question) => { + if (question.userAnswer === null) return "bg-yellow-500"; + return question.isCorrect ? "bg-green-500" : "bg-red-500"; + }; + + const getStatusText = (question: Question) => { + if (question.userAnswer === null) return "Skipped"; + return question.isCorrect ? "Correct" : "Incorrect"; + }; + + const getOptionClassName = (key: string, question: Question): string => { + const isCorrectAnswer = key === question.correctAnswer; + const isUserAnswer = key === question.userAnswer; + const isCorrectAndUserAnswer = isCorrectAnswer && isUserAnswer; + const isUserAnswerWrong = isUserAnswer && !isCorrectAnswer; + + if (isCorrectAndUserAnswer) { + return "bg-blue-900 text-white border-blue-900"; + } else if (isCorrectAnswer) { + return "bg-green-500 text-white border-green-500"; + } else if (isUserAnswerWrong) { + return "bg-red-500 text-white border-red-500"; + } + return "border-gray-300"; + }; + + // Handle browser back button + useEffect(() => { + const handlePopState = () => { + router.push("/unit"); + }; + + window.addEventListener("popstate", handlePopState); + return () => window.removeEventListener("popstate", handlePopState); + }, [router]); + + return ( +
+
+ {/* Header */} +
+ +
+ + {/* Main Content */} +
+ {/* Score Message */} +

+ {getScoreMessage(resultSheet.score)} +

+ + {/* Score Card */} +
+

Score:

+
+
+
+
+ + {resultSheet.score} + +
+
+ + {/* Solutions Section */} +
+

Solutions

+ +
+ {resultSheet.questions.map((question, idx) => ( +
+ {/* Question Header */} +
+

+ {idx + 1}. {question.question} +

+ + {getStatusText(question)} + +
+ + {/* Options */} +
+ {Object.entries(question.options).map(([key, option]) => ( +
+ + {key.toUpperCase()} + + {option} +
+ ))} +
+ + {/* Solution Divider */} +
+ + {/* Solution */} +
+

+ Solution: +

+

+ {question.solution} +

+
+
+ ))} +
+
+
+ + {/* Bottom Button */} +
+
+ +
+
+ + {/* Spacer for fixed button */} +
+
+ + +
+ ); }; -export default page; +export default ResultsPage; diff --git a/components/Header.tsx b/components/Header.tsx index ea1755f..b6a712b 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -5,15 +5,17 @@ import { ChevronLeft, Layers } from "lucide-react"; import { useTimer } from "@/context/TimerContext"; import styles from "@/css/Header.module.css"; -const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"; +const API_URL = "https://examjam-api.pptx704.com"; // You'll need to implement getToken for Next.js - could use cookies, localStorage, etc. const getToken = async () => { - // Replace with your token retrieval logic - if (typeof window !== "undefined") { - return localStorage.getItem("token") || sessionStorage.getItem("token"); + if (typeof window === "undefined") { + return null; } - return null; + + // Extract authToken from cookies + const match = document.cookie.match(/(?:^|;\s*)authToken=([^;]*)/); + return match ? decodeURIComponent(match[1]) : null; }; const Header = ({ diff --git a/lib/auth.ts b/lib/auth.ts index d50ceff..f9d4378 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -88,3 +88,13 @@ export const getTokenFromCookie = () => { export const clearAuthToken = () => { setCookie("authToken", null); }; + +export const getToken = async () => { + if (typeof window === "undefined") { + return null; + } + + // Extract authToken from cookies + const match = document.cookie.match(/(?:^|;\s*)authToken=([^;]*)/); + return match ? decodeURIComponent(match[1]) : null; +};