diff --git a/package.json b/package.json index f398c8a..a4dd4ce 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "embla-carousel-react": "^8.6.0", + "katex": "^0.16.28", "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-katex": "^3.1.0", "react-router-dom": "^7.12.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aca1ec2..1bc068c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: embla-carousel-react: specifier: ^8.6.0 version: 8.6.0(react@19.2.3) + katex: + specifier: ^0.16.28 + version: 0.16.28 lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.3) @@ -44,6 +47,9 @@ importers: react-dom: specifier: ^19.2.0 version: 19.2.3(react@19.2.3) + react-katex: + specifier: ^3.1.0 + version: 3.1.0(prop-types@15.8.1)(react@19.2.3) react-router-dom: specifier: ^7.12.0 version: 7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1145,6 +1151,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1395,6 +1405,10 @@ packages: engines: {node: '>=6'} hasBin: true + katex@0.16.28: + resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1479,6 +1493,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1511,6 +1529,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1550,6 +1572,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1559,6 +1584,15 @@ packages: peerDependencies: react: ^19.2.3 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-katex@3.1.0: + resolution: {integrity: sha512-At9uLOkC75gwn2N+ZXc5HD8TlATsB+3Hkp9OGs6uA8tM3dwZ3Wljn74Bk3JyHFPgSnesY/EMrIAB1WJwqZqejA==} + peerDependencies: + prop-types: ^15.8.1 + react: '>=15.3.2 <20' + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -2761,6 +2795,8 @@ snapshots: color-name@1.1.4: {} + commander@8.3.0: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -3008,6 +3044,10 @@ snapshots: json5@2.2.3: {} + katex@0.16.28: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3072,6 +3112,10 @@ snapshots: lodash.merge@4.6.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3100,6 +3144,8 @@ snapshots: node-releases@2.0.27: {} + object-assign@4.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3137,6 +3183,12 @@ snapshots: prelude-ls@1.2.1: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + punycode@2.3.1: {} react-dom@19.2.3(react@19.2.3): @@ -3144,6 +3196,14 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 + react-is@16.13.1: {} + + react-katex@3.1.0(prop-types@15.8.1)(react@19.2.3): + dependencies: + katex: 0.16.28 + prop-types: 15.8.1 + react: 19.2.3 + react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3): diff --git a/src/App.tsx b/src/App.tsx index 21d26dc..68a7169 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,5 @@ +import "katex/dist/katex.min.css"; + import { Home } from "./pages/student/Home"; import { createBrowserRouter, diff --git a/src/components/RenderQuestionText.tsx b/src/components/RenderQuestionText.tsx new file mode 100644 index 0000000..641e842 --- /dev/null +++ b/src/components/RenderQuestionText.tsx @@ -0,0 +1,19 @@ +import { BlockMath, InlineMath } from "react-katex"; + +export const renderQuestionText = (text: string) => { + const parts = text.split(/(\$\$.*?\$\$|\$.*?\$)/g); + + return ( + <> + {parts.map((part, index) => { + if (part.startsWith("$$")) { + return {part.slice(2, -2)}; + } + if (part.startsWith("$")) { + return {part.slice(1, -1)}; + } + return {part}; + })} + + ); +}; diff --git a/src/pages/student/practice/Test.tsx b/src/pages/student/practice/Test.tsx index 6d2ec79..cf78603 100644 --- a/src/pages/student/practice/Test.tsx +++ b/src/pages/student/practice/Test.tsx @@ -16,7 +16,7 @@ import { } from "lucide-react"; import { api } from "../../../utils/api"; import { useAuthStore } from "../../../stores/authStore"; -import type { Option, PracticeSheet, Question } from "../../../types/sheet"; +import type { PracticeSheet, Question } from "../../../types/sheet"; import { Button } from "../../../components/ui/button"; import { useSatExam } from "../../../stores/useSatExam"; import { useSatTimer } from "../../../hooks/useSatTimer"; @@ -25,6 +25,7 @@ import type { SubmitAnswer, } from "../../../types/session"; import { useAuthToken } from "../../../hooks/useAuthToken"; +import { renderQuestionText } from "../../../components/RenderQuestionText"; export const Test = () => { const navigate = useNavigate(); @@ -33,8 +34,9 @@ export const Test = () => { const [practiceSheet, setPracticeSheet] = useState( null, ); - const [answer, setAnswer] = useState(""); + // const [answer, setAnswer] = useState(""); const [answers, setAnswers] = useState>({}); + const [showNavigator, setShowNavigator] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [sessionId, setSessionId] = useState(null); @@ -47,10 +49,15 @@ export const Test = () => { const questionIndex = useSatExam((s) => s.questionIndex); const currentQuestion = currentModule?.questions[questionIndex]; + const currentAnswer = currentQuestion + ? (answers[currentQuestion.id] ?? "") + : ""; const resetExam = useSatExam((s) => s.resetExam); const nextQuestion = useSatExam((s) => s.nextQuestion); const prevQuestion = useSatExam((s) => s.prevQuestion); + const goToQuestion = useSatExam((s) => s.goToQuestion); + const finishExam = useSatExam((s) => s.finishExam); const startExam = async () => { @@ -184,10 +191,6 @@ export const Test = () => { if (!user) return; }, [sheetId]); - useEffect(() => { - setAnswer(""); - }, [questionIndex, currentModule?.module_id]); - // const isLastQuestion = // questionIndex === (currentModule?.questions.length ?? 0) - 1; @@ -201,7 +204,7 @@ export const Test = () => { return (
{question.options.map((option, index) => { - const isSelected = answer === option.id; + const isSelected = currentAnswer === option.id; return ( ); })} @@ -234,8 +242,13 @@ export const Test = () => { return (