diff --git a/index.html b/index.html index 97cd6ce..f79fac2 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - edbridge-scholars + Edbridge Scholars
diff --git a/src/App.tsx b/src/App.tsx index dcc78ca..21d26dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,19 @@ +import { Home } from "./pages/student/Home"; import { createBrowserRouter, Navigate, RouterProvider, } from "react-router-dom"; -import { Login } from "./pages/auth/Login"; -import { Home } from "./pages/student/Home"; -import { Practice } from "./pages/student/Practice"; -import { Rewards } from "./pages/student/Rewards"; -import { Profile } from "./pages/student/Profile"; -import { Lessons } from "./pages/student/Lessons"; import { ProtectedRoute } from "./components/ProtectedRoute"; -import { StudentLayout } from "./pages/student/StudentLayout"; -import { Test } from "./pages/student/practice/Test"; -import { Results } from "./pages/student/practice/Results"; +import { Login } from "./pages/auth/Login"; +import { Lessons } from "./pages/student/Lessons"; +import { Practice } from "./pages/student/Practice"; import { Pretest } from "./pages/student/practice/Pretest"; +import { Results } from "./pages/student/practice/Results"; +import { Test } from "./pages/student/practice/Test"; +import { Profile } from "./pages/student/Profile"; +import { Rewards } from "./pages/student/Rewards"; +import { StudentLayout } from "./pages/student/StudentLayout"; function App() { const router = createBrowserRouter([ @@ -51,21 +51,18 @@ function App() { { path: "practice/:sheetId", element: , - children: [ - { - path: "test", - element: , - }, - { - path: "results", - element: , - }, - ], }, - // more student subroutes here ], }, - // Add more subroutes here as needed + + { + path: "practice/:sheetId/test", + element: , + }, + { + path: "practice/:sheetId/test/results", + element: , + }, ], }, { diff --git a/src/components/examTimer.tsx b/src/components/examTimer.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/hooks/useAuthToken.ts b/src/hooks/useAuthToken.ts new file mode 100644 index 0000000..ccb09db --- /dev/null +++ b/src/hooks/useAuthToken.ts @@ -0,0 +1,26 @@ +// hooks/useAuthToken.ts +import { useMemo } from "react"; + +type AuthStorage = { + state?: { + token?: string; + }; +}; + +export function useAuthToken() { + const token = useMemo(() => { + const authStorage = localStorage.getItem("auth-storage"); + + if (!authStorage) return null; + + try { + const parsed: AuthStorage = JSON.parse(authStorage); + return parsed?.state?.token ?? null; + } catch (error) { + console.error("Failed to parse auth-storage:", error); + return null; + } + }, []); + + return token; +} diff --git a/src/hooks/useSatTimer.ts b/src/hooks/useSatTimer.ts new file mode 100644 index 0000000..22bd4d5 --- /dev/null +++ b/src/hooks/useSatTimer.ts @@ -0,0 +1,51 @@ +import { useEffect, useState } from "react"; +import { useSatExam } from "../stores/useSatExam"; + +export const useSatTimer = () => { + const phase = useSatExam((s) => s.phase); + const getRemainingTime = useSatExam((s) => s.getRemainingTime); + const startBreak = useSatExam((s) => s.startBreak); + const skipBreak = useSatExam((s) => s.skipBreak); + const finishExam = useSatExam((s) => s.finishExam); + + const currentModule = useSatExam((s) => s.currentModuleQuestions); + + const [time, setTime] = useState(0); + + // ✅ reset timer when phase or module changes + useEffect(() => { + setTime(getRemainingTime()); + }, [phase, currentModule?.module_id]); + + useEffect(() => { + if (phase === "IDLE" || phase === "FINISHED") return; + + const interval = setInterval(() => { + const remaining = getRemainingTime(); + setTime(remaining); + + if (remaining === 0) { + clearInterval(interval); + + if (phase === "BREAK") { + // ✅ break ended → go back to module + skipBreak(); + return; + } + + if (phase === "MODULE") { + // ⚠️ IMPORTANT: + // Timer should NOT load next module automatically. + // Instead, finish exam UI or let backend decide. + + finishExam(); + return; + } + } + }, 1000); + + return () => clearInterval(interval); + }, [phase, getRemainingTime, skipBreak, finishExam]); + + return time; +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3c757b0..914f230 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -75,3 +75,9 @@ export function getRandomColor() { ]; return colors[Math.floor(Math.random() * colors.length)]; } + +export const formatTime = (seconds: number) => { + const m = Math.floor(seconds / 60); + const s = seconds % 60; + return `${m}:${s.toString().padStart(2, "0")}`; +}; diff --git a/src/pages/student/Home.tsx b/src/pages/student/Home.tsx index ceba9f0..234f454 100644 --- a/src/pages/student/Home.tsx +++ b/src/pages/student/Home.tsx @@ -39,10 +39,10 @@ export const Home = () => { (sheet) => sheet.user_status === "NOT_STARTED", ); const inProgress = sheets.filter( - (sheet) => sheet.user_status === "in-progress", + (sheet) => sheet.user_status === "IN_PROGRESS", ); const completed = sheets.filter( - (sheet) => sheet.user_status === "completed", + (sheet) => sheet.user_status === "COMPLETED", ); setNotStartedSheets(notStarted); @@ -83,100 +83,99 @@ export const Home = () => { }; return ( -
-
-

- Welcome, {user?.name || "Student"} +
+

+ Welcome, {user?.name || "Student"} +

+
+ +
+ +
+
+
+

+ Pick up where you left off

-
- -
- -
-
-
-

- Pick up where you left off -

-
-
- - - - All - - - Not Started - - - Completed - - - -
- {practiceSheets.length > 0 ? ( - practiceSheets.map((sheet) => ( - - - - {sheet?.title} - - - {sheet?.description} - - - -

- {formatStatus(sheet?.user_status)} -

- - {sheet?.modules_count} modules - -
- -

- {sheet?.time_limit} minutes -

-
- - - -
- )) - ) : ( -
-

- No Practice Sheets available. -

-
- )} -
-
- -
- {notStartedSheets.map((sheet) => ( + {inProgressSheets.length > 0 ? ( + inProgressSheets.map((sheet) => ( + + + + {sheet?.title} + + + {sheet?.description} + + + +

+ {formatStatus(sheet?.user_status)} +

+ + {sheet?.modules_count} modules + +
+ +

+ {sheet?.time_limit} minutes +

+
+ + + +
+ )) + ) : ( + +

+ You don't have any practice sheets in progress. Why not start one? +

+
+ )} +
+
+ + + + All + + + Not Started + + + Completed + + + +
+ {practiceSheets.length > 0 ? ( + practiceSheets.map((sheet) => ( @@ -187,10 +186,12 @@ export const Home = () => { -

Not Started

+

+ {formatStatus(sheet?.user_status)} +

{sheet?.modules_count} modules @@ -202,17 +203,64 @@ export const Home = () => {
- ))} -
-
- + )) + ) : ( +
+

+ No Practice Sheets available. +

+
+ )} +

+ + +
+ {notStartedSheets.map((sheet) => ( + + + + {sheet?.title} + + + {sheet?.description} + + + +

Not Started

+ + {sheet?.modules_count} modules + +
+ +

+ {sheet?.time_limit} minutes +

+
+ + + +
+ ))} +
+
+ +
{completedSheets.length > 0 ? ( completedSheets.map((sheet) => ( @@ -257,46 +305,46 @@ export const Home = () => {
)} -
- + + + + + +
+

+ SAT Preparation Tips +

+
+
+ +

+ Practice regularly with official SAT materials +

+
+
+ +

+ Review your mistakes and learn from them +

+
+
+ +

Focus on your weak areas

+
+
+ +

+ Take full-length practice tests +

+
+
+ +

+ Get plenty of rest before the test day +

+
-
-
-

- SAT Preparation Tips -

-
-
- -

- Practice regularly with official SAT materials -

-
-
- -

- Review your mistakes and learn from them -

-
-
- -

Focus on your weak areas

-
-
- -

- Take full-length practice tests -

-
-
- -

- Get plenty of rest before the test day -

-
-
-
- - +
+ ); }; diff --git a/src/pages/student/Lessons.tsx b/src/pages/student/Lessons.tsx index c7e506c..781e743 100644 --- a/src/pages/student/Lessons.tsx +++ b/src/pages/student/Lessons.tsx @@ -42,19 +42,47 @@ export const Lessons = () => { - - - Video Thumbnail - - - Video Title - Video Description - - +
+ + + Video Thumbnail + + + Video Title + Video Description + + + + + Video Thumbnail + + + Video Title + Video Description + + + + + Video Thumbnail + + + Video Title + Video Description + + +
diff --git a/src/pages/student/Practice.tsx b/src/pages/student/Practice.tsx index 0e02189..9cf1ee7 100644 --- a/src/pages/student/Practice.tsx +++ b/src/pages/student/Practice.tsx @@ -36,13 +36,13 @@ export const Practice = () => { flex-row" >
- + See where you stand - +

Test your knowledge with an adaptive practice test.

@@ -53,67 +53,73 @@ export const Practice = () => {
-
+

Practice your way

- - -
- -
-
- Targeted Practice - - Focus on what matters - -
- -
- +
+ + +
+
- -
-
- - -
- -
-
- Drills - - Train speed and accuracy - -
- -
- +
+ + Targeted Practice + + + Focus on what matters +
- - - - - -
- -
-
- Hard Test Modules - - Focus on what matters - -
- -
- + +
+ +
+
+ + + + +
+
- -
-
+
+ Drills + + Train speed and accuracy + +
+ +
+ +
+
+ + + + +
+ +
+
+ + Hard Test Modules + + + Focus on what matters + +
+ +
+ +
+
+
+
+
); diff --git a/src/pages/student/Profile.tsx b/src/pages/student/Profile.tsx index cfa6a7f..96a7975 100644 --- a/src/pages/student/Profile.tsx +++ b/src/pages/student/Profile.tsx @@ -12,7 +12,7 @@ export const Profile = () => { }; return ( -
+

Profile

{user?.name}

diff --git a/src/pages/student/practice/Pretest.tsx b/src/pages/student/practice/Pretest.tsx index 1a965d4..f85b8ee 100644 --- a/src/pages/student/practice/Pretest.tsx +++ b/src/pages/student/practice/Pretest.tsx @@ -1,9 +1,16 @@ import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; +import { Outlet, replace, useParams } from "react-router-dom"; import { api } from "../../../utils/api"; import { useAuthStore } from "../../../stores/authStore"; import type { PracticeSheet } from "../../../types/sheet"; -import { CircleQuestionMark, Clock, Layers, Tag } from "lucide-react"; +import { + CircleQuestionMark, + Clock, + Layers, + Loader, + Loader2, + Tag, +} from "lucide-react"; import { Carousel, CarouselContent, @@ -11,6 +18,7 @@ import { type CarouselApi, } from "../../../components/ui/carousel"; import { Button } from "../../../components/ui/button"; +import { useNavigate } from "react-router-dom"; export const Pretest = () => { const user = useAuthStore((state) => state.user); @@ -18,13 +26,18 @@ export const Pretest = () => { const [carouselApi, setCarouselApi] = useState(); const [current, setCurrent] = useState(0); const [count, setCount] = useState(0); + const navigate = useNavigate(); const [practiceSheet, setPracticeSheet] = useState( null, ); function handleStartTest(sheetId: string) { - console.log("Starting test for Practice Sheet. ID: ", sheetId); + if (!sheetId) { + console.error("Sheet ID is required to start the test."); + return; + } + navigate(`/student/practice/${sheetId}/test`, { replace: true }); } useEffect(() => { @@ -67,35 +80,43 @@ export const Pretest = () => { {practiceSheet?.description}

-
-
- -
-

- {practiceSheet?.time_limit} -

-

Minutes

+ {practiceSheet ? ( +
+
+ +
+

+ {practiceSheet?.time_limit} +

+

Minutes

+
-
-
- -
-

- {practiceSheet?.modules.length} -

-

Modules

+
+ +
+

+ {practiceSheet?.modules.length} +

+

Modules

+
-
-
- -
-

- {practiceSheet?.questions_count} -

-

Questions

+
+ +
+

+ {practiceSheet?.questions_count} +

+

Questions

+
-
-
+
+ ) : ( +
+
+ +
+
+ )} {practiceSheet ? ( @@ -161,7 +182,10 @@ export const Pretest = () => { ) ) : ( -
+
+
+ +

Loading...

@@ -191,8 +215,15 @@ export const Pretest = () => { onClick={() => handleStartTest(practiceSheet?.id!)} variant="outline" className="font-satoshi rounded-3xl w-full text-lg py-8 bg-linear-to-br from-purple-500 to-purple-600 text-white active:bg-linear-to-br active:from-purple-600 active:to-purple-700 active:translate-y-1" + disabled={!practiceSheet} > - Start Test + {practiceSheet ? ( + "Start Test" + ) : ( +
+ +
+ )}
); diff --git a/src/pages/student/practice/Results.tsx b/src/pages/student/practice/Results.tsx index ec57a20..915fc97 100644 --- a/src/pages/student/practice/Results.tsx +++ b/src/pages/student/practice/Results.tsx @@ -1,3 +1,12 @@ +import { useNavigate } from "react-router-dom"; +import { Button } from "../../../components/ui/button"; + export const Results = () => { - return
Results
; + const navigate = useNavigate(); + return ( +
+ Your results go here + +
+ ); }; diff --git a/src/pages/student/practice/Test.tsx b/src/pages/student/practice/Test.tsx index b6fcbc7..fca3964 100644 --- a/src/pages/student/practice/Test.tsx +++ b/src/pages/student/practice/Test.tsx @@ -1,3 +1,406 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../../../components/ui/card"; +import { + Clock, + Layers, + CircleQuestionMark, + Check, + Loader2, +} from "lucide-react"; +import { api } from "../../../utils/api"; +import { useAuthStore } from "../../../stores/authStore"; +import type { Option, PracticeSheet } from "../../../types/sheet"; +import { Button } from "../../../components/ui/button"; +import { useSatExam } from "../../../stores/useSatExam"; +import { useSatTimer } from "../../../hooks/useSatTimer"; +import type { + SessionModuleQuestions, + SubmitAnswer, +} from "../../../types/session"; +import { useAuthToken } from "../../../hooks/useAuthToken"; + export const Test = () => { - return
Test
; + const navigate = useNavigate(); + const { user } = useAuthStore(); + const token = useAuthToken(); + const [practiceSheet, setPracticeSheet] = useState( + null, + ); + const [answers, setAnswers] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const [sessionId, setSessionId] = useState(null); + const { sheetId } = useParams<{ sheetId: string }>(); + + const time = useSatTimer(); + const phase = useSatExam((s) => s.phase); + // const moduleIndex = useSatExam((s) => s.moduleIndex); + const currentModule = useSatExam((s) => s.currentModuleQuestions); + const questionIndex = useSatExam((s) => s.questionIndex); + + const currentQuestion = currentModule?.questions[questionIndex]; + + const resetExam = useSatExam((s) => s.resetExam); + const startSatExam = useSatExam((s) => s.startExam); + const nextQuestion = useSatExam((s) => s.nextQuestion); + const prevQuestion = useSatExam((s) => s.prevQuestion); + const finishExam = useSatExam((s) => s.finishExam); + + const startExam = async () => { + if (!user || !sheetId) return; + + try { + const response = await api.startSession(token as string, { + sheet_id: sheetId, + mode: "MODULE", + topic_ids: practiceSheet?.topics.map((t) => t.id) ?? [], + difficulty: practiceSheet?.difficulty ?? "EASY", + question_count: 2, + time_limit_minutes: practiceSheet?.time_limit ?? 0, + }); + + setSessionId(response.id); + + await loadSessionQuestions(response.id); + + // ✅ NOW start module phase + useSatExam.getState().startExam(); + } catch (error) { + console.error("Failed to start exam session:", error); + } + }; + + const loadSessionQuestions = async (sessionId: string) => { + if (!token) return; + + try { + const data = await api.fetchSessionQuestions(token, sessionId); + + const module: SessionModuleQuestions = { + module_id: data.module_id, + module_title: data.module_title, + time_limit_minutes: data.time_limit_minutes * 60, + questions: data.questions.map((q) => ({ + id: q.id, + text: q.text, + context: q.context, + context_image_url: q.context_image_url, + type: q.type, + section: q.section, + image_url: q.image_url, + index: q.index, + difficulty: q.difficulty, + correct_answer: q.correct_answer, + explanation: q.explanation, + topics: q.topics, + options: q.options, + })), + }; + + useSatExam.getState().setModuleQuestions(module); + } catch (err) { + console.error("Failed to load session questions:", err); + } + }; + + const handleNext = async () => { + if (!currentQuestion || !selectedOption || !sessionId) return; + + const selected = currentQuestion.options.find( + (opt) => opt.id === selectedOption, + ); + + if (!selected) return; + + const answerPayload: SubmitAnswer = { + question_id: currentQuestion.id, + answer_text: selected.text, + time_spent_seconds: 3, + }; + + setIsSubmitting(true); + + await api.submitAnswer(token!, sessionId, answerPayload); + + const isLastQuestion = + questionIndex === currentModule!.questions.length - 1; + + // ✅ normal question flow + if (!isLastQuestion) { + nextQuestion(); + setIsSubmitting(false); + return; + } + + // ✅ ask backend for next module + const next = await api.fetchNextModule(token!, sessionId); + + if (next?.finished) { + finishExam(); + } else { + await loadSessionQuestions(sessionId); + + // ✅ IMPORTANT: start break AFTER module loads + useSatExam.getState().startBreak(); + } + + setIsSubmitting(false); + }; + + useEffect(() => { + resetExam(); // ✅ important + }, [sheetId]); + + useEffect(() => { + if (phase === "FINISHED") { + const timer = setTimeout(() => { + navigate(`/student/practice/${sheetId}/test/results`, { + replace: true, + }); + }, 3000); + + return () => clearTimeout(timer); + } + }, [phase]); + + useEffect(() => { + if (!user) return; + }, [sheetId]); + + const [selectedOption, setSelectedOption] = useState(null); + + const isLastQuestion = + questionIndex === (currentModule?.questions.length ?? 0) - 1; + + const isFirstQuestion = questionIndex === 0; + + useEffect(() => { + setSelectedOption(null); + }, [questionIndex, currentModule?.module_id]); + + const renderOptions = (options?: Option[]) => { + if (!options || !Array.isArray(options)) { + return

No options available.

; + } + + const handleOptionClick = (option: Option) => { + setSelectedOption(option.id); + }; + + return ( +
+ {options.map((option, index) => { + const isSelected = selectedOption === option.id; + + return ( + + ); + })} +
+ ); + }; + + switch (phase) { + case "IDLE": + return ( +
+ + + + Ready to begin your test? + + +
+
+
+ +
+
+

+ {practiceSheet?.time_limit} +

+

Minutes

+
+
+
+
+ +
+
+

+ {practiceSheet?.questions_count} +

+

Questions

+
+
+
+
+ +
+
+

+ {practiceSheet?.modules.length} +

+

Modules

+
+
+
+
+
+ +

Before you begin:

+
+ + + This test will run on full screen mode for a distraction-free + experience + +
+
+ + + You can exit full-screen anytime by pressing Esc + +
+
+ + + Your progress will be saved automatically + +
+
+ + + You can take breaks using the "More" menu in the top right + +
+ +
+
+
+ ); + case "MODULE": + return ( +
+
+
+
+

+ {Math.floor(time / 60)}:{String(time % 60).padStart(2, "0")} +

+

+ {currentModule?.module_title} +

+ {/*

+ {practiceSheet?.modules[0].description} +

*/} +
+
+
+ {currentModule?.questions[0]?.context && ( +
+

+ {currentQuestion?.context} +

+
+ )} + +
+
+

+ {currentQuestion?.text} +

+
+
+ {renderOptions(currentQuestion?.options)} +
+
+ + + + + +
+
+
+ ); + case "BREAK": + return ( +
+ 🧘 Break Time +

Next module starts in {time}s

+ +
+ ); + + case "FINISHED": + return ( +
+ ⏰ Time’s Up! +

Redirecting to results...

+
+ ); + + default: + return null; + } }; diff --git a/src/stores/useSatExam.ts b/src/stores/useSatExam.ts new file mode 100644 index 0000000..237a101 --- /dev/null +++ b/src/stores/useSatExam.ts @@ -0,0 +1,127 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { type ExamPhase } from "../types/sheet"; +import type { SessionModuleQuestions } from "../types/session"; + +// interface SatExamState { +// modules: Module[]; +// phase: ExamPhase; +// moduleIndex: number; +// questionIndex: number; +// endTime: number | null; + +// startExam: () => void; +// setModuleQuestionss: (modules: Module[]) => void; + +// nextQuestion: () => void; +// prevQuestion: () => void; +// nextModule: () => void; +// nextPhase: () => void; +// skipBreak: () => void; +// getRemainingTime: () => number; +// finishExam: () => void; +// resetExam: () => void; +// replaceModules: (modules: Module[]) => void; +// } + +interface SatExamState { + currentModuleQuestions: SessionModuleQuestions | null; + phase: ExamPhase; + questionIndex: number; + endTime: number | null; + + setModuleQuestions: (module: SessionModuleQuestions) => void; + startExam: () => void; + nextQuestion: () => void; + prevQuestion: () => void; + + startBreak: () => void; + skipBreak: () => void; + finishExam: () => void; + resetExam: () => void; + getRemainingTime: () => number; +} + +const BREAK_DURATION = 30; // seconds + +export const useSatExam = create()( + persist( + (set, get) => ({ + currentModuleQuestions: null, + phase: "IDLE", + questionIndex: 0, + endTime: null, + + setModuleQuestions: (module: SessionModuleQuestions) => { + const endTime = Date.now() + module.time_limit_minutes * 1000; + + set({ + currentModuleQuestions: module, + questionIndex: 0, + endTime, + }); + }, + + startExam: () => { + set({ phase: "MODULE", questionIndex: 0 }); + }, + + nextQuestion: () => { + const { currentModuleQuestions, questionIndex } = get(); + const questions = currentModuleQuestions?.questions ?? []; + + if (questionIndex < questions.length - 1) { + set({ questionIndex: questionIndex + 1 }); + } + }, + + prevQuestion: () => { + const { questionIndex } = get(); + if (questionIndex > 0) { + set({ questionIndex: questionIndex - 1 }); + } + }, + + startBreak: () => { + const endTime = Date.now() + BREAK_DURATION * 1000; + + set((state) => ({ + phase: "BREAK", + endTime, + questionIndex: 0, // optional: reset question index for next module UX + })); + }, + + skipBreak: () => { + const module = get().currentModuleQuestions; + + if (!module) return; + + const endTime = Date.now() + module.time_limit_minutes * 1000; + + set({ + phase: "MODULE", + endTime, + questionIndex: 0, + }); + }, + + finishExam: () => set({ phase: "FINISHED", endTime: null }), + + getRemainingTime: () => { + const { endTime } = get(); + if (!endTime) return 0; + return Math.max(0, Math.floor((endTime - Date.now()) / 1000)); + }, + + resetExam: () => + set({ + currentModuleQuestions: null, + phase: "IDLE", + questionIndex: 0, + endTime: null, + }), + }), + { name: "sat-exam-storage" }, + ), +); diff --git a/src/types/session.ts b/src/types/session.ts new file mode 100644 index 0000000..d6075fa --- /dev/null +++ b/src/types/session.ts @@ -0,0 +1,66 @@ +import type { Question } from "./sheet"; + +type Answer = { + id: string; + question_id: string; + answer_text: string; + is_correct: boolean; + marked_for_review: false; +}; + +/** + * SessionRequest interface + * `/sessions/` + */ + +export interface SessionRequest { + sheet_id: string; + mode: string; + topic_ids: string[]; + difficulty: string; + question_count: number; + time_limit_minutes: number; +} + +export interface SessionResponse { + id: string; + practice_sheet_id: string; + status: string; + current_module_index: number; + current_model_id: string; + current_module_title: string; + answers: Answer[]; + started_at: Date; + score: number; +} + +export type SubmitAnswer = { + question_id: string; + answer_text: string; + time_spent_seconds: number; +}; + +export interface SessionAnswerResponse { + status: string; + feedback: { + is_correct: boolean; + correct_answer: string; + explanation: string; + }; +} + +export interface SessionQuestionsResponse { + session_id: string; + module_id: string; + module_title: string; + time_limit_minutes: number; + questions: Question[]; +} + +export interface SessionModuleQuestions { + session_id?: string; + module_id: string; + module_title: string; + time_limit_minutes: number; + questions: Question[]; +} diff --git a/src/types/sheet.ts b/src/types/sheet.ts index 3a85211..5e9d9ce 100644 --- a/src/types/sheet.ts +++ b/src/types/sheet.ts @@ -4,6 +4,8 @@ interface CreatedBy { email: string; } +export type ExamPhase = "IDLE" | "MODULE" | "BREAK" | "FINISHED"; + export interface Subject { name: string; section: string; @@ -13,7 +15,15 @@ export interface Subject { parent_name: string; } +export interface Option { + text: string; + image_url: string; + sequence_order: number; + id: string; +} + export interface Question { + difficulty: string; text: string; context: string; context_image_url: string; @@ -22,7 +32,7 @@ export interface Question { image_url: string; index: number; id: string; - options: any[]; + options: Option[]; topics: Topic[]; correct_answer: string; explanation: string; diff --git a/src/utils/api.ts b/src/utils/api.ts index a219b62..ae5270f 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,3 +1,10 @@ +import type { + SessionAnswerResponse, + SessionQuestionsResponse, + SessionRequest, + SessionResponse, + SubmitAnswer, +} from "../types/session"; import type { PracticeSheet } from "../types/sheet"; const API_URL = "https://ed-dev-api.omukk.dev"; @@ -121,6 +128,60 @@ class ApiClient { token, ); } -} + async startSession( + token: string, + sessionData: SessionRequest, + ): Promise { + return this.authenticatedRequest(`/sessions/`, token, { + method: "POST", + body: JSON.stringify(sessionData), + }); + } + + async fetchSessionQuestions( + token: string, + sessionId: string, + ): Promise { + return this.authenticatedRequest( + `/sessions/${sessionId}/questions/`, + token, + ); + } + + async submitAnswer( + token: string, + sessionId: string, + answerSubmissionData: SubmitAnswer, + ): Promise { + return this.authenticatedRequest( + `/sessions/${sessionId}/answer/`, + token, + { + method: "POST", + body: JSON.stringify(answerSubmissionData), + }, + ); + } + + async fetchNextModule(token: string, sessionId: string): Promise { + return this.authenticatedRequest( + `/sessions/${sessionId}/next-module/`, + token, + { + method: "POST", + }, + ); + } + + async fetchSessionStateById( + token: string, + sessionId: string, + ): Promise { + return this.authenticatedRequest( + `/sessions/${sessionId}`, + token, + ); + } +} export const api = new ApiClient(API_URL);