fix(api): fix api endpoint logic #6

This commit is contained in:
Dacca Retro
2025-08-18 17:48:32 +06:00
parent 58d4d14a51
commit d74b81e962
5 changed files with 306 additions and 206 deletions

View File

@ -6,159 +6,70 @@ import { useEffect, useState } from "react";
import Header from "@/components/Header"; import Header from "@/components/Header";
import DestructibleAlert from "@/components/DestructibleAlert"; import DestructibleAlert from "@/components/DestructibleAlert";
import BackgroundWrapper from "@/components/BackgroundWrapper"; import BackgroundWrapper from "@/components/BackgroundWrapper";
import { API_URL } from "@/lib/auth"; import { API_URL, getToken } from "@/lib/auth";
import { Loader, RefreshCw } from "lucide-react"; import { Loader, RefreshCw } from "lucide-react";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { useAuth } from "@/context/AuthContext"; import { useAuth } from "@/context/AuthContext";
import { Question } from "@/types/exam";
interface Mock { type Subject = {
id: string;
title: string;
rating: number;
}
export default function PaperScreen() {
const router = useRouter();
const searchParams = useSearchParams();
const name = searchParams.get("name") || "";
const {user} = useAuth()
const [questions, setQuestions] = useState<Mock[] | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState<boolean>(false);
const [componentKey, setComponentKey] = useState<number>(0);
interface Subject {
subject_id: string; subject_id: string;
name: string; name: string;
unit: string; unit: string;
} };
const [subjects, setSubjects] = useState<Subject[]>([ export default function PaperScreen() {
{ const router = useRouter();
subject_id: uuidv4(), const searchParams = useSearchParams();
name: "Biology", const { user } = useAuth();
unit: "Science"
},
{
subject_id: uuidv4(),
name: "Chemistry",
unit: "Science"
},
{
subject_id: uuidv4(),
name: "Physics",
unit: "Science"
},
{
subject_id: uuidv4(),
name: "Accounting",
unit: "Business"
},
{
subject_id: uuidv4(),
name: "Finance",
unit: "Business"
},
{
subject_id: uuidv4(),
name: "Marketing",
unit: "Business"
},
{
subject_id: uuidv4(),
name: "History",
unit: "Humanities"
},
{
subject_id: uuidv4(),
name: "Geography",
unit: "Humanities"
},
{
subject_id: uuidv4(),
name: "Sociology",
unit: "Humanities"
},
]);
const [subjects, setSubjects] = useState<Subject[]>([]);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState<boolean>(false);
async function fetchSubjects() {
setRefreshing(true);
try {
const token = await getToken();
const response = await fetch(`${API_URL}/subjects/`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to fetch subjects");
}
// async function fetchMocks() { const fetchedSubjects: Subject[] = await response.json();
// try { setSubjects(fetchedSubjects);
// const questionResponse = await fetch(`${API_URL}/mocks`, { } catch (error) {
// method: "GET", setErrorMsg(
// }); "Error fetching subjects: " +
// const fetchedQuestionData: Mock[] = await questionResponse.json(); (error instanceof Error ? error.message : "Unknown error")
// setQuestions(fetchedQuestionData); );
// } catch (error) { } finally {
// setErrorMsg(error instanceof Error ? error.message : "An error occurred"); setRefreshing(false);
// } }
// } }
// useEffect(() => { useEffect(() => {
// if (name) { const fetchData = async () => {
// fetchMocks(); if (await getToken()) {
// } fetchSubjects();
// }, [name]); }
};
fetchData();
}, []);
const onRefresh = async () => { const onRefresh = async () => {
setRefreshing(true); fetchSubjects();
setSubjects([{
subject_id: uuidv4(),
name: "Biology",
unit: "Science"
},
{
subject_id: uuidv4(),
name: "Chemistry",
unit: "Science"
},
{
subject_id: uuidv4(),
name: "Physics",
unit: "Science"
},
{
subject_id: uuidv4(),
name: "Accounting",
unit: "Business Studies"
},
{
subject_id: uuidv4(),
name: "Finance",
unit: "Business Studies"
},
{
subject_id: uuidv4(),
name: "Marketing",
unit: "Business Studies"
},
{
subject_id: uuidv4(),
name: "History",
unit: "Humanities"
},
{
subject_id: uuidv4(),
name: "Geography",
unit: "Humanities"
},
{
subject_id: uuidv4(),
name: "Sociology",
unit: "Humanities"
}])
setComponentKey((prevKey) => prevKey + 1);
setTimeout(() => {
setRefreshing(false);
}, 1000);
}; };
if (errorMsg) { if (errorMsg) {
return ( return (
<BackgroundWrapper> <BackgroundWrapper>
<Header displayTabTitle={name} /> <Header displayTabTitle="Subjects" />
<div className="overflow-y-auto"> <div className="overflow-y-auto">
<div className="mt-5 px-5"> <div className="mt-5 px-5">
<DestructibleAlert text={errorMsg} extraStyles="" /> <DestructibleAlert text={errorMsg} extraStyles="" />
@ -182,32 +93,32 @@ const [subjects, setSubjects] = useState<Subject[]>([
<div> <div>
<Header displayTabTitle="Subjects" /> <Header displayTabTitle="Subjects" />
<div className="mx-10 pt-10 overflow-y-auto"> <div className="mx-10 pt-10 overflow-y-auto">
<h1 className="text-2xl font-semibold mb-5">{user?.preparation_unit}</h1> <h1 className="text-2xl font-semibold mb-5">
{user?.preparation_unit}
</h1>
<div className="border border-[#c0dafc] flex flex-col gap-4 w-full rounded-[25px] p-4 mb-20"> <div className="border border-[#c0dafc] flex flex-col gap-4 w-full rounded-[25px] p-4 mb-20">
{subjects ? ( {subjects.length > 0 ? (
subjects subjects
.filter(subject => subject.unit === user?.preparation_unit) .filter((subject) => subject.unit === user?.preparation_unit)
.map((subject) => ( .map((subject) => (
<div key={subject.subject_id}> <div key={subject.subject_id}>
<button <button
onClick={() => onClick={() =>
router.push( router.push(
`/exam/pretest?unitname=${name}&id=${subject.subject_id}&title=${subject.name}` `/exam/pretest?test_id=${subject.subject_id}`
) )
} }
className="w-full border-2 border-[#B0C2DA] py-4 rounded-[10px] px-6 space-y-2 text-left hover:bg-gray-50 transition-colors" className="w-full border-2 border-[#B0C2DA] py-4 rounded-[10px] px-6 space-y-2 text-left hover:bg-gray-50 transition-colors"
> >
<h3 className="text-xl font-medium">{subject.name}</h3> <h3 className="text-xl font-medium">{subject.name}</h3>
<p className="text-md font-normal"> <p className="text-md font-normal">Rating: 8/10</p>
Rating: 8/10 </button>
</p> </div>
</button> ))
</div>
))
) : ( ) : (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
<p className="text-xl font-medium text-center">Loading...</p> <p className="text-xl font-medium text-center">Loading...</p>
</div> </div>
)} )}
</div> </div>

View File

@ -7,27 +7,17 @@ import DestructibleAlert from "@/components/DestructibleAlert";
import BackgroundWrapper from "@/components/BackgroundWrapper"; import BackgroundWrapper from "@/components/BackgroundWrapper";
import { API_URL } from "@/lib/auth"; import { API_URL } from "@/lib/auth";
import { useExam } from "@/context/ExamContext"; import { useExam } from "@/context/ExamContext";
import { Exam } from "@/types/exam"; import { Test } from "@/types/exam";
import { Metadata } from "@/types/exam";
interface Metadata {
metadata: {
quantity: number;
type: string;
duration: number;
marking: string;
};
}
function PretestPageContent() { function PretestPageContent() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [examData, setExamData] = useState<Exam>(); const [examData, setExamData] = useState<Test>();
const { startExam, setCurrentExam } = useExam(); const { startExam, setCurrentExam } = useExam();
// Get params from URL search params // Get params from URL search params
const id = searchParams.get("id") || ""; const id = searchParams.get("id") || "";
const title = searchParams.get("title") || "";
const rating = searchParams.get("rating") || "";
const [metadata, setMetadata] = useState<Metadata | null>(null); const [metadata, setMetadata] = useState<Metadata | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -67,7 +57,7 @@ function PretestPageContent() {
<BackgroundWrapper> <BackgroundWrapper>
<div className="min-h-screen"> <div className="min-h-screen">
<div className="mx-10 mt-10"> <div className="mx-10 mt-10">
<button onClick={() => router.push("/unit")} className="mb-4"> <button onClick={() => router.push("/subjects")} className="mb-4">
<ArrowLeft size={30} color="black" /> <ArrowLeft size={30} color="black" />
</button> </button>
<DestructibleAlert text={error} extraStyles="" /> <DestructibleAlert text={error} extraStyles="" />
@ -77,6 +67,24 @@ function PretestPageContent() {
); );
} }
if (loading) {
return (
<BackgroundWrapper>
<div className="min-h-screen">
<div className="mx-10 pt-10">
<button onClick={() => router.push("/subjects")} className="mb-4">
<ArrowLeft size={30} color="black" />
</button>
<div className="flex flex-col items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
<p className="text-lg font-medium text-gray-900">Loading...</p>
</div>
</div>
</div>
</BackgroundWrapper>
);
}
function handleStartExam() { function handleStartExam() {
if (!examData) return; if (!examData) return;

View File

@ -72,7 +72,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
if (!res.ok) { if (!res.ok) {
throw new Error("Failed to fetch user info"); throw new Error("Failed to fetch user info");
} }
console.log(API_URL)
const data: UserData = await res.json(); const data: UserData = await res.json();
setUser(data); setUser(data);
} catch (error) { } catch (error) {

View File

@ -0,0 +1,175 @@
{
"questions": [
{
"correct_answer": "Assets = Liabilities + Equity",
"difficulty": "Easy",
"explanation": "This is the basic accounting equation.",
"is_randomized": true,
"options": [
"Assets = Liabilities + Equity",
"Assets = Revenue - Expenses",
"Assets = Liabilities - Equity",
"Assets = Equity - Liabilities"
],
"question": "What is the basic accounting equation?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "a1b2c3d4-e5f6-7890-abcd-1234567890ab",
"type": "Single"
},
{
"correct_answer": "Balance Sheet",
"difficulty": "Easy",
"explanation": "A balance sheet shows a company's assets, liabilities, and equity at a specific point in time.",
"is_randomized": true,
"options": [
"Balance Sheet",
"Income Statement",
"Cash Flow Statement",
"Statement of Retained Earnings"
],
"question": "Which financial statement shows a company's assets, liabilities, and equity?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "b2c3d4e5-f678-9012-abcd-2345678901bc",
"type": "Single"
},
{
"correct_answer": "Revenue - Expenses",
"difficulty": "Easy",
"explanation": "Net income is calculated by subtracting expenses from revenue.",
"is_randomized": true,
"options": [
"Revenue - Expenses",
"Assets - Liabilities",
"Equity + Liabilities",
"Revenue + Expenses"
],
"question": "How is net income calculated?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "c3d4e5f6-7890-1234-abcd-3456789012cd",
"type": "Single"
},
{
"correct_answer": "Accounts Receivable",
"difficulty": "Easy",
"explanation": "Accounts receivable represents money owed to a company by its customers.",
"is_randomized": true,
"options": [
"Accounts Receivable",
"Accounts Payable",
"Inventory",
"Prepaid Expenses"
],
"question": "Which account represents money owed to a company by its customers?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "d4e5f678-9012-3456-abcd-4567890123de",
"type": "Single"
},
{
"correct_answer": "Depreciation",
"difficulty": "Medium",
"explanation": "Depreciation is the allocation of the cost of a tangible asset over its useful life.",
"is_randomized": true,
"options": [
"Depreciation",
"Amortization",
"Depletion",
"Appreciation"
],
"question": "What is the allocation of the cost of a tangible asset over its useful life called?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "e5f67890-1234-5678-abcd-5678901234ef",
"type": "Single"
},
{
"correct_answer": "Accrual Basis",
"difficulty": "Medium",
"explanation": "Accrual basis accounting recognizes revenues and expenses when they are incurred, not when cash is exchanged.",
"is_randomized": true,
"options": [
"Accrual Basis",
"Cash Basis",
"Modified Cash Basis",
"Tax Basis"
],
"question": "Which accounting method recognizes revenues and expenses when they are incurred, regardless of when cash is exchanged?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "f6789012-3456-7890-abcd-6789012345fa",
"type": "Single"
},
{
"correct_answer": "Liabilities",
"difficulty": "Easy",
"explanation": "Liabilities are obligations that a company owes to outside parties.",
"is_randomized": true,
"options": [
"Liabilities",
"Assets",
"Equity",
"Revenue"
],
"question": "What are obligations that a company owes to outside parties called?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "a7890123-4567-8901-abcd-7890123456ab",
"type": "Single"
},
{
"correct_answer": "Double-entry",
"difficulty": "Medium",
"explanation": "Double-entry accounting means every transaction affects at least two accounts.",
"is_randomized": true,
"options": [
"Double-entry",
"Single-entry",
"Triple-entry",
"Quadruple-entry"
],
"question": "What is the accounting system called where every transaction affects at least two accounts?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "b8901234-5678-9012-abcd-8901234567bc",
"type": "Single"
},
{
"correct_answer": "Owner's Equity",
"difficulty": "Easy",
"explanation": "Owner's equity represents the owner's claims to the assets of the business.",
"is_randomized": true,
"options": [
"Owner's Equity",
"Liabilities",
"Revenue",
"Expenses"
],
"question": "What is the owner's claim to the assets of a business called?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "c9012345-6789-0123-abcd-9012345678cd",
"type": "Single"
},
{
"correct_answer": "Matching Principle",
"difficulty": "Medium",
"explanation": "The matching principle requires that expenses be matched with related revenues.",
"is_randomized": true,
"options": [
"Matching Principle",
"Revenue Recognition Principle",
"Cost Principle",
"Conservatism Principle"
],
"question": "Which accounting principle requires that expenses be matched with related revenues?",
"source_type": "Mock",
"subject_id": "f1c1d54a-0b29-44e2-8f95-ecf9f9e6d8b7",
"topic_id": "d0123456-7890-1234-abcd-0123456789de",
"type": "Single"
}
]
}

77
types/exam.d.ts vendored
View File

@ -1,55 +1,62 @@
export interface Question { export type Metadata = {
id: string; test_id: string;
question: string; total_possible_score: number;
options?: Record<string, string>; deduction: number;
type: "multiple-choice" | "text" | "boolean" | undefined; num_questions: number;
correctAnswer: string | undefined; name: string;
solution?: string | undefined; start_time: Date;
time_limit_minutes: number;
} }
export interface Exam { export type Question = {
id: string; question_id: string;
title: string; question: string;
description: string; options: string[];
type: string;
}
export interface Test {
metadata: Metadata;
questions: Question[]; questions: Question[];
timeLimit?: number;
passingScore?: number;
} }
export interface ExamAnswer { type Answers = number | null;
questionId: string;
answer: string;
timestamp: Date;
}
export interface ExamAttempt { export interface TestAttempt {
examId: string; user_id: string;
exam: Exam; test_id: string;
answers: ExamAnswer[]; attempt_id: string;
startTime: Date; start_time: Date;
endTime?: Date; end_time: Date;
score: number; user_questions: {
passed?: boolean; question_id: string;
apiResponse?: any; question: string;
totalQuestions: number; options: string[];
type: string;
};
user_answers: Record<string, Answers>;
correct_answers: Record<string, Answers>;
correct_answers_count: number;
wrong_answers_count: number;
skipped_questions_count: number;
} }
export interface ExamContextType { export interface ExamContextType {
currentExam: Exam | null; currentExam: Test | null;
currentAttempt: ExamAttempt | null; currentAttempt: TestAttempt | null;
isHydrated: boolean; isHydrated: boolean;
isInitialized: boolean; isInitialized: boolean;
// Actions // Actions
setCurrentExam: (exam: Exam) => void; setCurrentExam: (exam: Test) => void;
startExam: (exam?: Exam) => void; startExam: (exam?: Test) => void;
setAnswer: (questionId: string, answer: any) => void; setAnswer: (questionId: string, answer: Answers) => void;
submitExam: () => ExamAttempt | null; submitExam: () => TestAttempt | null;
clearExam: () => void; clearExam: () => void;
setApiResponse: (response: any) => void; setApiResponse: (response: any) => void;
// Getters // Getters
getAnswer: (questionId: string) => any; getAnswer: (questionId: string) => Answers;
getProgress: () => number; getProgress: () => number;
isExamStarted: () => boolean; isExamStarted: () => boolean;
isExamCompleted: () => boolean; isExamCompleted: () => boolean;