From 6f4f2a86684413e78fe4ae6864c6a5b6325f18bf Mon Sep 17 00:00:00 2001 From: shafin-r Date: Thu, 21 Aug 2025 14:19:55 +0600 Subject: [PATCH] feat(ui): add topic, subject screen fix(api): fix api endpoint logic for pretest screen --- app/(tabs)/categories/mocks/page.tsx | 142 ++++++++++++++++++ app/(tabs)/categories/page.tsx | 75 +++++++++ app/(tabs)/{ => categories}/subjects/page.tsx | 2 +- app/(tabs)/{ => categories}/topics/page.tsx | 8 +- app/(tabs)/home/page.tsx | 122 +++++++-------- app/(tabs)/layout.tsx | 2 +- app/exam/pretest/page.tsx | 93 ++++++++---- types/exam.d.ts | 30 +++- 8 files changed, 369 insertions(+), 105 deletions(-) create mode 100644 app/(tabs)/categories/mocks/page.tsx create mode 100644 app/(tabs)/categories/page.tsx rename app/(tabs)/{ => categories}/subjects/page.tsx (98%) rename app/(tabs)/{ => categories}/topics/page.tsx (95%) diff --git a/app/(tabs)/categories/mocks/page.tsx b/app/(tabs)/categories/mocks/page.tsx new file mode 100644 index 0000000..f0e3ce0 --- /dev/null +++ b/app/(tabs)/categories/mocks/page.tsx @@ -0,0 +1,142 @@ +"use client"; + +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, getToken } from "@/lib/auth"; +import { Loader, RefreshCw } from "lucide-react"; +import { useAuth } from "@/context/AuthContext"; + +type Mock = { + test_id: string; + name: string; + type: string; + num_questions: number; + time_limit_minutes: number; + deduction: number; + total_possible_score: number; + unit: string; +}; + +export default function MockScreen() { + const router = useRouter(); + const { user } = useAuth(); + + const [mocks, setMocks] = useState([]); + const [errorMsg, setErrorMsg] = useState(null); + const [refreshing, setRefreshing] = useState(false); + async function fetchMocks() { + setRefreshing(true); + try { + const token = await getToken(); + const response = await fetch(`${API_URL}/tests/mock/`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch topics"); + } + + const fetchedMocks: Mock[] = await response.json(); + setMocks(fetchedMocks); + } catch (error) { + setErrorMsg( + "Error fetching mocks: " + + (error instanceof Error ? error.message : "Unknown error") + ); + } finally { + setRefreshing(false); + } + } + + useEffect(() => { + const fetchData = async () => { + if (await getToken()) { + fetchMocks(); + } + }; + fetchData(); + }, []); + + const onRefresh = async () => { + fetchMocks(); + }; + + if (errorMsg) { + return ( + +
+
+
+ +
+
+ +
+
+ + ); + } + + return ( + +
+
+
+

+ {user?.preparation_unit} +

+
+ {mocks.length > 0 ? ( + mocks.map((mocks) => ( +
+ +
+ )) + ) : ( +
+
+

Loading...

+
+ )} +
+
+ +
+
+
+ {/* */} +
+ ); +} diff --git a/app/(tabs)/categories/page.tsx b/app/(tabs)/categories/page.tsx new file mode 100644 index 0000000..753c695 --- /dev/null +++ b/app/(tabs)/categories/page.tsx @@ -0,0 +1,75 @@ +"use client"; + +import BackgroundWrapper from "@/components/BackgroundWrapper"; +import Header from "@/components/Header"; +import React from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; + +const CategoriesPage = () => { + const router = useRouter(); + + return ( + +
+
+
+ + + + + + + +
+
+
+ ); +}; + +export default CategoriesPage; diff --git a/app/(tabs)/subjects/page.tsx b/app/(tabs)/categories/subjects/page.tsx similarity index 98% rename from app/(tabs)/subjects/page.tsx rename to app/(tabs)/categories/subjects/page.tsx index e087ced..802b136 100644 --- a/app/(tabs)/subjects/page.tsx +++ b/app/(tabs)/categories/subjects/page.tsx @@ -103,7 +103,7 @@ export default function PaperScreen() { -
-
- - -
-
- - -
+
+ + + + + + +
diff --git a/app/(tabs)/layout.tsx b/app/(tabs)/layout.tsx index 66ac321..086af23 100644 --- a/app/(tabs)/layout.tsx +++ b/app/(tabs)/layout.tsx @@ -11,7 +11,7 @@ const tabs = [ { name: "Home", href: "/home", component: }, { name: "Categories", - href: "/subjects", + href: "/categories", component: , }, { name: "Bookmark", href: "/bookmark", component: }, diff --git a/app/exam/pretest/page.tsx b/app/exam/pretest/page.tsx index 65f008f..47eb4f8 100644 --- a/app/exam/pretest/page.tsx +++ b/app/exam/pretest/page.tsx @@ -5,40 +5,66 @@ import { Suspense, 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"; +import { API_URL, getToken } from "@/lib/auth"; import { useExam } from "@/context/ExamContext"; import { Test } from "@/types/exam"; -import { Metadata } from "@/types/exam"; +import { Metadata, MockMeta, SubjectMeta, TopicMeta } from "@/types/exam"; + +type MetadataType = "mock" | "subject" | "topic"; + +type MetadataMap = { + mock: MockMeta; + subject: SubjectMeta; + topic: TopicMeta; +}; function PretestPageContent() { const router = useRouter(); const searchParams = useSearchParams(); - const [examData, setExamData] = useState(); const { startExam, setCurrentExam } = useExam(); // Get params from URL search params - const id = searchParams.get("id") || ""; + const id = searchParams.get("test_id") || ""; + const typeParam = searchParams.get("type"); + const type = + typeParam === "mock" || typeParam === "subject" || typeParam === "topic" + ? typeParam + : null; - const [metadata, setMetadata] = useState(null); + const [metadata, setMetadata] = useState( + null + ); const [loading, setLoading] = useState(true); const [error, setError] = useState(); + function fetchMetadata( + type: T, + data: unknown + ): MetadataMap[T] { + return data as MetadataMap[T]; // you'd validate in real code + } + useEffect(() => { async function fetchQuestions() { - if (!id) return; + if (!id || !type) return; try { setLoading(true); - const questionResponse = await fetch(`${API_URL}/mock/${id}`, { + const token = await getToken(); + const questionResponse = await fetch(`${API_URL}/tests/${type}/${id}`, { method: "GET", + headers: { + authorization: `Bearer ${token}`, + }, }); + if (!questionResponse.ok) { throw new Error("Failed to fetch questions"); } - const data = await questionResponse.json(); - const fetchedMetadata: Metadata = data; - setExamData(data); + const data = await questionResponse.json(); + const fetchedMetadata = fetchMetadata(type, data.metadata); + setMetadata(fetchedMetadata); } catch (error) { console.error(error); @@ -47,17 +73,19 @@ function PretestPageContent() { setLoading(false); } } - if (id) { - fetchQuestions(); - } - }, [id]); + + fetchQuestions(); + }, [id, type]); if (error) { return (
- @@ -72,12 +100,15 @@ function PretestPageContent() {
-
-

Loading...

+

Loading...

@@ -85,13 +116,13 @@ function PretestPageContent() { ); } - function handleStartExam() { - if (!examData) return; + // function handleStartExam() { + // if (!examData) return; - setCurrentExam(examData); - startExam(examData); - router.push(`/exam/${id}?time=${metadata?.metadata.duration}`); - } + // setCurrentExam(examData); + // startExam(examData); + // router.push(`/exam/${id}?time=${metadata?.metadata.duration}`); + // } return (
@@ -102,10 +133,12 @@ function PretestPageContent() { -

{title}

+

+ {metadata.name} +

- Rating: {rating} / 10 + Rating: 8 / 10

@@ -113,10 +146,10 @@ function PretestPageContent() {

- {metadata.metadata.quantity} + {metadata.num_questions}

- {metadata.metadata.type} + Multiple Choice Questions

@@ -125,7 +158,7 @@ function PretestPageContent() {

- {metadata.metadata.duration} mins + {String(metadata.time_limit_minutes)} mins

Time Taken

@@ -135,7 +168,7 @@ function PretestPageContent() {

- {metadata.metadata.marking} + {metadata.deduction} marks

From each wrong answer @@ -188,7 +221,7 @@ function PretestPageContent() {