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 DestructibleAlert from "@/components/DestructibleAlert";
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 { v4 as uuidv4 } from "uuid";
import { useAuth } from "@/context/AuthContext";
import { Question } from "@/types/exam";
interface Mock {
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 {
type Subject = {
subject_id: string;
name: string;
unit: string;
};
export default function PaperScreen() {
const router = useRouter();
const searchParams = useSearchParams();
const { user } = useAuth();
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");
}
const [subjects, setSubjects] = useState<Subject[]>([
{
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"
},
{
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 fetchedSubjects: Subject[] = await response.json();
setSubjects(fetchedSubjects);
} catch (error) {
setErrorMsg(
"Error fetching subjects: " +
(error instanceof Error ? error.message : "Unknown error")
);
} finally {
setRefreshing(false);
}
}
// async function fetchMocks() {
// try {
// const questionResponse = await fetch(`${API_URL}/mocks`, {
// method: "GET",
// });
// const fetchedQuestionData: Mock[] = await questionResponse.json();
// setQuestions(fetchedQuestionData);
// } catch (error) {
// setErrorMsg(error instanceof Error ? error.message : "An error occurred");
// }
// }
// useEffect(() => {
// if (name) {
// fetchMocks();
// }
// }, [name]);
useEffect(() => {
const fetchData = async () => {
if (await getToken()) {
fetchSubjects();
}
};
fetchData();
}, []);
const onRefresh = async () => {
setRefreshing(true);
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);
fetchSubjects();
};
if (errorMsg) {
return (
<BackgroundWrapper>
<Header displayTabTitle={name} />
<Header displayTabTitle="Subjects" />
<div className="overflow-y-auto">
<div className="mt-5 px-5">
<DestructibleAlert text={errorMsg} extraStyles="" />
@ -182,25 +93,25 @@ const [subjects, setSubjects] = useState<Subject[]>([
<div>
<Header displayTabTitle="Subjects" />
<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">
{subjects ? (
{subjects.length > 0 ? (
subjects
.filter(subject => subject.unit === user?.preparation_unit)
.filter((subject) => subject.unit === user?.preparation_unit)
.map((subject) => (
<div key={subject.subject_id}>
<button
onClick={() =>
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"
>
<h3 className="text-xl font-medium">{subject.name}</h3>
<p className="text-md font-normal">
Rating: 8/10
</p>
<p className="text-md font-normal">Rating: 8/10</p>
</button>
</div>
))

View File

@ -7,27 +7,17 @@ import DestructibleAlert from "@/components/DestructibleAlert";
import BackgroundWrapper from "@/components/BackgroundWrapper";
import { API_URL } from "@/lib/auth";
import { useExam } from "@/context/ExamContext";
import { Exam } from "@/types/exam";
interface Metadata {
metadata: {
quantity: number;
type: string;
duration: number;
marking: string;
};
}
import { Test } from "@/types/exam";
import { Metadata } from "@/types/exam";
function PretestPageContent() {
const router = useRouter();
const searchParams = useSearchParams();
const [examData, setExamData] = useState<Exam>();
const [examData, setExamData] = useState<Test>();
const { startExam, setCurrentExam } = useExam();
// Get params from URL search params
const id = searchParams.get("id") || "";
const title = searchParams.get("title") || "";
const rating = searchParams.get("rating") || "";
const [metadata, setMetadata] = useState<Metadata | null>(null);
const [loading, setLoading] = useState(true);
@ -67,7 +57,7 @@ function PretestPageContent() {
<BackgroundWrapper>
<div className="min-h-screen">
<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" />
</button>
<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() {
if (!examData) return;

View File

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

75
types/exam.d.ts vendored
View File

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