feat(test): add jump to question functionality
This commit is contained in:
@ -19,9 +19,11 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"katex": "^0.16.28",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-katex": "^3.1.0",
|
||||||
"react-router-dom": "^7.12.0",
|
"react-router-dom": "^7.12.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
|||||||
60
pnpm-lock.yaml
generated
60
pnpm-lock.yaml
generated
@ -35,6 +35,9 @@ importers:
|
|||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: ^8.6.0
|
specifier: ^8.6.0
|
||||||
version: 8.6.0(react@19.2.3)
|
version: 8.6.0(react@19.2.3)
|
||||||
|
katex:
|
||||||
|
specifier: ^0.16.28
|
||||||
|
version: 0.16.28
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.562.0
|
specifier: ^0.562.0
|
||||||
version: 0.562.0(react@19.2.3)
|
version: 0.562.0(react@19.2.3)
|
||||||
@ -44,6 +47,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
version: 19.2.3(react@19.2.3)
|
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:
|
react-router-dom:
|
||||||
specifier: ^7.12.0
|
specifier: ^7.12.0
|
||||||
version: 7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
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:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
commander@8.3.0:
|
||||||
|
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
@ -1395,6 +1405,10 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
katex@0.16.28:
|
||||||
|
resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
@ -1479,6 +1493,10 @@ packages:
|
|||||||
lodash.merge@4.6.2:
|
lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
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:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
@ -1511,6 +1529,10 @@ packages:
|
|||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
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:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -1550,6 +1572,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
prop-types@15.8.1:
|
||||||
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1559,6 +1584,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.3
|
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:
|
react-refresh@0.18.0:
|
||||||
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
|
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -2761,6 +2795,8 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
commander@8.3.0: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
@ -3008,6 +3044,10 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
katex@0.16.28:
|
||||||
|
dependencies:
|
||||||
|
commander: 8.3.0
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
@ -3072,6 +3112,10 @@ snapshots:
|
|||||||
|
|
||||||
lodash.merge@4.6.2: {}
|
lodash.merge@4.6.2: {}
|
||||||
|
|
||||||
|
loose-envify@1.4.0:
|
||||||
|
dependencies:
|
||||||
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
@ -3100,6 +3144,8 @@ snapshots:
|
|||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
@ -3137,6 +3183,12 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
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: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
react-dom@19.2.3(react@19.2.3):
|
react-dom@19.2.3(react@19.2.3):
|
||||||
@ -3144,6 +3196,14 @@ snapshots:
|
|||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
scheduler: 0.27.0
|
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-refresh@0.18.0: {}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3):
|
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3):
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
import { Home } from "./pages/student/Home";
|
import { Home } from "./pages/student/Home";
|
||||||
import {
|
import {
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
|
|||||||
19
src/components/RenderQuestionText.tsx
Normal file
19
src/components/RenderQuestionText.tsx
Normal file
@ -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 <BlockMath key={index}>{part.slice(2, -2)}</BlockMath>;
|
||||||
|
}
|
||||||
|
if (part.startsWith("$")) {
|
||||||
|
return <InlineMath key={index}>{part.slice(1, -1)}</InlineMath>;
|
||||||
|
}
|
||||||
|
return <span key={index}>{part}</span>;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { api } from "../../../utils/api";
|
import { api } from "../../../utils/api";
|
||||||
import { useAuthStore } from "../../../stores/authStore";
|
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 { Button } from "../../../components/ui/button";
|
||||||
import { useSatExam } from "../../../stores/useSatExam";
|
import { useSatExam } from "../../../stores/useSatExam";
|
||||||
import { useSatTimer } from "../../../hooks/useSatTimer";
|
import { useSatTimer } from "../../../hooks/useSatTimer";
|
||||||
@ -25,6 +25,7 @@ import type {
|
|||||||
SubmitAnswer,
|
SubmitAnswer,
|
||||||
} from "../../../types/session";
|
} from "../../../types/session";
|
||||||
import { useAuthToken } from "../../../hooks/useAuthToken";
|
import { useAuthToken } from "../../../hooks/useAuthToken";
|
||||||
|
import { renderQuestionText } from "../../../components/RenderQuestionText";
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -33,8 +34,9 @@ export const Test = () => {
|
|||||||
const [practiceSheet, setPracticeSheet] = useState<PracticeSheet | null>(
|
const [practiceSheet, setPracticeSheet] = useState<PracticeSheet | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [answer, setAnswer] = useState<string>("");
|
// const [answer, setAnswer] = useState<string>("");
|
||||||
const [answers, setAnswers] = useState<Record<string, string>>({});
|
const [answers, setAnswers] = useState<Record<string, string>>({});
|
||||||
|
const [showNavigator, setShowNavigator] = useState<boolean>(false);
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
@ -47,10 +49,15 @@ export const Test = () => {
|
|||||||
const questionIndex = useSatExam((s) => s.questionIndex);
|
const questionIndex = useSatExam((s) => s.questionIndex);
|
||||||
|
|
||||||
const currentQuestion = currentModule?.questions[questionIndex];
|
const currentQuestion = currentModule?.questions[questionIndex];
|
||||||
|
const currentAnswer = currentQuestion
|
||||||
|
? (answers[currentQuestion.id] ?? "")
|
||||||
|
: "";
|
||||||
|
|
||||||
const resetExam = useSatExam((s) => s.resetExam);
|
const resetExam = useSatExam((s) => s.resetExam);
|
||||||
const nextQuestion = useSatExam((s) => s.nextQuestion);
|
const nextQuestion = useSatExam((s) => s.nextQuestion);
|
||||||
const prevQuestion = useSatExam((s) => s.prevQuestion);
|
const prevQuestion = useSatExam((s) => s.prevQuestion);
|
||||||
|
const goToQuestion = useSatExam((s) => s.goToQuestion);
|
||||||
|
|
||||||
const finishExam = useSatExam((s) => s.finishExam);
|
const finishExam = useSatExam((s) => s.finishExam);
|
||||||
|
|
||||||
const startExam = async () => {
|
const startExam = async () => {
|
||||||
@ -184,10 +191,6 @@ export const Test = () => {
|
|||||||
if (!user) return;
|
if (!user) return;
|
||||||
}, [sheetId]);
|
}, [sheetId]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAnswer("");
|
|
||||||
}, [questionIndex, currentModule?.module_id]);
|
|
||||||
|
|
||||||
// const isLastQuestion =
|
// const isLastQuestion =
|
||||||
// questionIndex === (currentModule?.questions.length ?? 0) - 1;
|
// questionIndex === (currentModule?.questions.length ?? 0) - 1;
|
||||||
|
|
||||||
@ -201,7 +204,7 @@ export const Test = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{question.options.map((option, index) => {
|
{question.options.map((option, index) => {
|
||||||
const isSelected = answer === option.id;
|
const isSelected = currentAnswer === option.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -211,7 +214,12 @@ export const Test = () => {
|
|||||||
? "bg-linear-to-br from-purple-400 to-purple-500 text-white"
|
? "bg-linear-to-br from-purple-400 to-purple-500 text-white"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setAnswer(option.id)}
|
onClick={() =>
|
||||||
|
setAnswers((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[question.id]: option.id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 rounded-full ${
|
className={`px-2 py-1 rounded-full ${
|
||||||
@ -222,7 +230,7 @@ export const Test = () => {
|
|||||||
>
|
>
|
||||||
{"ABCD"[index]}
|
{"ABCD"[index]}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
<span>{option.text}</span>
|
<span>{renderQuestionText(option.text)}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -234,8 +242,13 @@ export const Test = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 pt-4">
|
<div className="flex flex-col gap-3 pt-4">
|
||||||
<textarea
|
<textarea
|
||||||
value={answer}
|
value={currentAnswer}
|
||||||
onChange={(e) => setAnswer(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setAnswers((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[question.id]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
placeholder="Type your answer here..."
|
placeholder="Type your answer here..."
|
||||||
className="w-full min-h-30 border rounded-xl px-4 py-3 text-lg font-satoshi focus:outline-none focus:ring-2 focus:ring-purple-400"
|
className="w-full min-h-30 border rounded-xl px-4 py-3 text-lg font-satoshi focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||||
/>
|
/>
|
||||||
@ -246,13 +259,13 @@ export const Test = () => {
|
|||||||
switch (phase) {
|
switch (phase) {
|
||||||
case "IDLE":
|
case "IDLE":
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen px-8 py-8 w-full space-y-6">
|
<main className="min-h-screen px-8 py-8 w-full space-y-6 flex flex-col items-center justify-center">
|
||||||
<Card className="">
|
<Card className="">
|
||||||
<CardHeader className="space-y-6">
|
<CardHeader className="space-y-6">
|
||||||
<CardTitle className="font-satoshi text-4xl">
|
<CardTitle className="font-satoshi text-4xl">
|
||||||
Ready to begin your test?
|
Ready to begin your test?
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
{/* <CardDescription>
|
||||||
<section className="flex justify-between gap-6 px-4">
|
<section className="flex justify-between gap-6 px-4">
|
||||||
<div className="flex flex-col justify-center items-center gap-4">
|
<div className="flex flex-col justify-center items-center gap-4">
|
||||||
<div className="w-fit bg-cyan-100 p-2 rounded-full">
|
<div className="w-fit bg-cyan-100 p-2 rounded-full">
|
||||||
@ -291,7 +304,7 @@ export const Test = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</CardDescription>
|
</CardDescription> */}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<h2 className="font-satoshi-bold text-2xl">Before you begin:</h2>
|
<h2 className="font-satoshi-bold text-2xl">Before you begin:</h2>
|
||||||
@ -335,7 +348,7 @@ export const Test = () => {
|
|||||||
return (
|
return (
|
||||||
<main className="">
|
<main className="">
|
||||||
<section className="w-full flex flex-col space-y-4 min-h-screen">
|
<section className="w-full flex flex-col space-y-4 min-h-screen">
|
||||||
<section className="fixed top-0 left-0 right-0 bg-white border-b border-gray-300 px-8 pt-8 pb-4 space-y-2 z-10">
|
<section className="fixed top-0 left-0 right-0 bg-white border-b border-purple-300 px-8 pt-8 pb-4 space-y-2 z-10">
|
||||||
<header className="space-y-2 flex flex-col items-center">
|
<header className="space-y-2 flex flex-col items-center">
|
||||||
<h2 className="font-satoshi-bold text-3xl w-fit">
|
<h2 className="font-satoshi-bold text-3xl w-fit">
|
||||||
{Math.floor(time / 60)}:{String(time % 60).padStart(2, "0")}
|
{Math.floor(time / 60)}:{String(time % 60).padStart(2, "0")}
|
||||||
@ -348,8 +361,8 @@ export const Test = () => {
|
|||||||
</p> */}
|
</p> */}
|
||||||
</header>
|
</header>
|
||||||
</section>
|
</section>
|
||||||
<hr className="border-gray-300" />
|
<div className="border border-purple-300"></div>
|
||||||
{currentModule?.questions[0]?.context && (
|
{currentQuestion?.context && (
|
||||||
<section className="h-100 overflow-y-auto px-10 pt-30">
|
<section className="h-100 overflow-y-auto px-10 pt-30">
|
||||||
<p className="font-satoshi tracking-wide text-lg">
|
<p className="font-satoshi tracking-wide text-lg">
|
||||||
{currentQuestion?.context}
|
{currentQuestion?.context}
|
||||||
@ -357,15 +370,16 @@ export const Test = () => {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="border border-gray-300"></div>
|
<div className="border border-purple-200"></div>
|
||||||
<section
|
<section
|
||||||
className={`px-10 ${currentQuestion?.context ? "pt-26" : "pt-26"}`}
|
className={`px-10 ${currentQuestion?.context ? "" : "pt-26"}`}
|
||||||
>
|
>
|
||||||
<p className="font-satoshi-medium text-xl">
|
<p className="font-satoshi-medium text-xl">
|
||||||
{currentQuestion?.text}
|
{currentQuestion?.text &&
|
||||||
|
renderQuestionText(currentQuestion.text)}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section className="overflow-y-auto px-10 pb-20">
|
<section className="overflow-y-auto max-h-100 md:max-h-fit px-10 pb-20">
|
||||||
{renderAnswerInput(currentQuestion)}
|
{renderAnswerInput(currentQuestion)}
|
||||||
</section>
|
</section>
|
||||||
<section className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-300 py-4 flex justify-evenly">
|
<section className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-300 py-4 flex justify-evenly">
|
||||||
@ -377,10 +391,63 @@ export const Test = () => {
|
|||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button className="px-8 border rounded-full py-3 font-satoshi-medium text-black">
|
{/* <button className="px-8 border rounded-full py-3 font-satoshi-medium text-black">
|
||||||
Menu
|
Menu
|
||||||
|
</button> */}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNavigator(true)}
|
||||||
|
className="px-8 border rounded-full py-3 font-satoshi-medium text-black"
|
||||||
|
>
|
||||||
|
Go to
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{showNavigator && (
|
||||||
|
<div className="fixed inset-0 bg-black/40 flex justify-center items-center z-50">
|
||||||
|
<div className="bg-white rounded-2xl w-[500px] max-h-[70vh] p-6 flex flex-col gap-4 shadow-xl">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h2 className="text-xl font-satoshi-bold">
|
||||||
|
Jump to Question
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNavigator(false)}
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-6 gap-3 overflow-y-auto">
|
||||||
|
{currentModule?.questions.map((q, idx) => {
|
||||||
|
const isCurrent = idx === questionIndex;
|
||||||
|
const isAnswered = !!answers[q.id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={q.id}
|
||||||
|
onClick={() => {
|
||||||
|
goToQuestion(idx);
|
||||||
|
setShowNavigator(false);
|
||||||
|
}}
|
||||||
|
className={`w-12 h-12 rounded-lg flex items-center justify-center font-satoshi-medium border transition
|
||||||
|
${
|
||||||
|
isCurrent
|
||||||
|
? "bg-purple-600 text-white border-purple-600"
|
||||||
|
: isAnswered
|
||||||
|
? "bg-green-100 border-green-400 text-green-700"
|
||||||
|
: "bg-white border-gray-300 hover:bg-gray-100"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{idx + 1}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
@ -398,8 +465,8 @@ export const Test = () => {
|
|||||||
);
|
);
|
||||||
case "BREAK":
|
case "BREAK":
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col justify-center items-center text-3xl gap-6">
|
<div className="min-h-screen flex flex-col justify-center items-center text-3xl gap-6 font-satoshi">
|
||||||
🧘 Break Time
|
Break Time
|
||||||
<p className="text-lg mt-4">Next module starts in {time}s</p>
|
<p className="text-lg mt-4">Next module starts in {time}s</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => useSatExam.getState().skipBreak()}
|
onClick={() => useSatExam.getState().skipBreak()}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ interface SatExamState {
|
|||||||
startExam: () => void;
|
startExam: () => void;
|
||||||
nextQuestion: () => void;
|
nextQuestion: () => void;
|
||||||
prevQuestion: () => void;
|
prevQuestion: () => void;
|
||||||
|
goToQuestion: (index: number) => void;
|
||||||
|
|
||||||
startBreak: () => void;
|
startBreak: () => void;
|
||||||
skipBreak: () => void;
|
skipBreak: () => void;
|
||||||
@ -82,6 +83,15 @@ export const useSatExam = create<SatExamState>()(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
goToQuestion: (index: number) =>
|
||||||
|
set((state) => {
|
||||||
|
const total = state.currentModuleQuestions?.questions.length ?? 0;
|
||||||
|
|
||||||
|
if (index < 0 || index >= total) return state;
|
||||||
|
|
||||||
|
return { questionIndex: index };
|
||||||
|
}),
|
||||||
|
|
||||||
startBreak: () => {
|
startBreak: () => {
|
||||||
const endTime = Date.now() + BREAK_DURATION * 1000;
|
const endTime = Date.now() + BREAK_DURATION * 1000;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user