Fix UI issues, timer logic, theme switcher, view solutions, and markdown rendering
This commit is contained in:
BIN
backend/pragativeer-prd.pdf
Normal file
BIN
backend/pragativeer-prd.pdf
Normal file
Binary file not shown.
BIN
cbt_test.html
Normal file
BIN
cbt_test.html
Normal file
Binary file not shown.
70
package-lock.json
generated
70
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^3.9.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"zustand": "^5.0.14"
|
||||
@@ -4673,6 +4674,31 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-raw": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
|
||||
"integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"@ungap/structured-clone": "^1.0.0",
|
||||
"hast-util-from-parse5": "^8.0.0",
|
||||
"hast-util-to-parse5": "^8.0.0",
|
||||
"html-void-elements": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"parse5": "^7.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0",
|
||||
"web-namespaces": "^2.0.0",
|
||||
"zwitch": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-jsx-runtime": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||
@@ -4700,6 +4726,25 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-parse5": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz",
|
||||
"integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"web-namespaces": "^2.0.0",
|
||||
"zwitch": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-text": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
|
||||
@@ -4779,6 +4824,16 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.6",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.6.tgz",
|
||||
@@ -7409,6 +7464,21 @@
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-raw": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
|
||||
"integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-raw": "^9.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-math": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^3.9.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"zustand": "^5.0.14"
|
||||
|
||||
9
parse.js
Normal file
9
parse.js
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkMath from "remark-math";
|
||||
|
||||
const text = "C) $2 \\text{ m}$";
|
||||
const tree = unified().use(remarkParse).use(remarkMath).parse(text);
|
||||
console.log(JSON.stringify(tree, null, 2));
|
||||
|
||||
18
parse2.js
Normal file
18
parse2.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
|
||||
const text = "The velocity of a particle moving along the x-axis is given by $v(t) = \\int_{0}^{t} (3\\tau^2 - 2\\tau) d\\tau$. What is the displacement of the particle at $t = 2$ seconds if it starts from the origin?";
|
||||
const tree = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkMath)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeKatex, { strict: "warn", throwOnError: false })
|
||||
.use(rehypeStringify)
|
||||
.processSync(text);
|
||||
console.log(tree.toString());
|
||||
|
||||
BIN
prd_dump.txt
Normal file
BIN
prd_dump.txt
Normal file
Binary file not shown.
1385
prd_dump_utf8.txt
Normal file
1385
prd_dump_utf8.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,15 @@
|
||||
import { useEffect, useState, use, useRef } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Clock, Flag, ChevronLeft, ChevronRight, Pause as PauseIcon, MoreVertical, Calculator as CalculatorIcon, FlaskConical, Edit3, XCircle, Play, Shield, AlertTriangle } from "lucide-react"
|
||||
import { Clock, Flag, ChevronLeft, ChevronRight, Pause as PauseIcon, MoreVertical, Calculator as CalculatorIcon, FlaskConical, Edit3, XCircle, Play, Shield, AlertTriangle, Moon, Sun } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useCBTStore, QuestionStatus } from "@/store/cbtStore"
|
||||
import { useExamTimer } from "@/hooks/useExamTimer"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import remarkMath from "remark-math"
|
||||
import rehypeKatex from "rehype-katex"
|
||||
import rehypeRaw from "rehype-raw"
|
||||
import "katex/dist/katex.min.css"
|
||||
import { Calculator } from "@/components/CBT/Calculator"
|
||||
import { Scratchpad } from "@/components/CBT/Scratchpad"
|
||||
@@ -35,9 +37,9 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
const [showPeriodicTable, setShowPeriodicTable] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [showSubmitConfirm, setShowSubmitConfirm] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
// Anti-cheat state
|
||||
const [testStarted, setTestStarted] = useState(false);
|
||||
const [violationCount, setViolationCount] = useState(0);
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
const hasShownWarning = useRef(false);
|
||||
@@ -45,6 +47,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
|
||||
const {
|
||||
initExam,
|
||||
startExam,
|
||||
questions,
|
||||
answers,
|
||||
currentQuestionIndex,
|
||||
@@ -57,7 +60,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
getAttemptPayload,
|
||||
submitAttemptToServer,
|
||||
resetExamState,
|
||||
status
|
||||
status,
|
||||
attemptId
|
||||
} = useCBTStore();
|
||||
|
||||
const { formattedTime, isTimedOut } = useExamTimer();
|
||||
@@ -109,14 +113,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
|
||||
// Track time spent per question
|
||||
useEffect(() => {
|
||||
if (testStarted) {
|
||||
if (status === 'IN_PROGRESS') {
|
||||
setQuestionStartTime(Date.now());
|
||||
}
|
||||
}, [currentQuestionIndex, testStarted]);
|
||||
}, [currentQuestionIndex, status]);
|
||||
|
||||
// Anti-Cheat: Tab switching listener
|
||||
useEffect(() => {
|
||||
if (!testStarted || status === 'SUBMITTED' || status === 'TIMED_OUT') return;
|
||||
if (status !== 'IN_PROGRESS') return;
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
@@ -126,7 +130,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
}, [testStarted, status]);
|
||||
}, [status]);
|
||||
|
||||
// Handle side-effects of violation count changing
|
||||
useEffect(() => {
|
||||
@@ -155,7 +159,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
} catch (e) {
|
||||
console.warn("Fullscreen API failed", e);
|
||||
}
|
||||
setTestStarted(true);
|
||||
startExam();
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
@@ -199,7 +203,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
|
||||
if (!mounted || questions.length === 0) return <div className="p-8">Loading Exam Engine...</div>;
|
||||
|
||||
if (!testStarted && status !== 'SUBMITTED' && status !== 'TIMED_OUT') {
|
||||
if (status === 'NOT_STARTED') {
|
||||
return (
|
||||
<div className="h-screen flex flex-col items-center justify-center bg-background p-8 text-center animate-in fade-in duration-500">
|
||||
<div className="max-w-2xl bg-secondary/10 border border-border p-8 rounded-2xl shadow-2xl">
|
||||
@@ -216,6 +220,17 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
<li>Ensure your internet connection is stable before starting.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 p-4 bg-muted rounded-xl flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-semibold">Preferred Theme</p>
|
||||
<p className="text-sm text-muted-foreground">Choose dark or light mode for the exam</p>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||
{theme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button size="lg" className="w-full text-lg font-bold h-14 shadow-[0_0_20px_-5px_var(--color-primary)]" onClick={startTest}>
|
||||
I Understand, Start Test
|
||||
</Button>
|
||||
@@ -231,6 +246,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
return (
|
||||
<ExamResult
|
||||
payload={payload}
|
||||
attemptId={attemptId}
|
||||
onRetake={() => {
|
||||
resetExamState();
|
||||
window.location.reload();
|
||||
@@ -346,7 +362,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
</div>
|
||||
|
||||
<div className="prose prose-neutral dark:prose-invert max-w-none text-lg leading-relaxed text-foreground font-medium">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex, rehypeRaw]}>
|
||||
{currentQuestion.text_en}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
@@ -377,7 +393,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
{letter}
|
||||
</div>
|
||||
<span className={`text-base font-semibold flex-1 ${isSelected ? 'text-primary' : ''}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex, rehypeRaw]}>
|
||||
{option}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
@@ -402,7 +418,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose prose-neutral dark:prose-invert max-w-none text-lg leading-relaxed text-foreground font-medium">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex, rehypeRaw]}>
|
||||
{currentQuestion.text_hi}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
@@ -433,7 +449,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
{letter}
|
||||
</div>
|
||||
<span className={`text-base font-semibold flex-1 ${isSelected ? 'text-primary' : ''}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex, rehypeRaw]}>
|
||||
{option}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
|
||||
@@ -132,7 +132,7 @@ export default function CBTPage() {
|
||||
</Badge>
|
||||
{exam.exam_pattern && (
|
||||
<Badge variant="outline" className="bg-primary/20 text-primary border-primary/30 uppercase tracking-wider text-[10px]">
|
||||
{exam.exam_pattern}
|
||||
{exam.exam_pattern === 'JEE_Main' ? 'JEE Main' : exam.exam_pattern === 'JEE_Advanced' ? 'JEE Advanced' : exam.exam_pattern}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -218,7 +218,7 @@ export default function CBTPage() {
|
||||
<div className="font-bold text-base md:text-lg text-[#00F0FF]">{Math.round(attempt.accuracy)}%</div>
|
||||
</div>
|
||||
<Link href={`/dashboard/cbt/history/${attempt.id}`}>
|
||||
<Button variant="outline" className="rounded-xl border-border hover:bg-secondary hover:text-foreground">
|
||||
<Button variant="outline" className="rounded-xl border-border hover:bg-primary hover:text-primary-foreground">
|
||||
View Solutions
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -78,6 +78,26 @@ export default function CommunityPage() {
|
||||
return true
|
||||
})
|
||||
|
||||
if (!hasAccessTo("ask-doubts-from-tutors")) {
|
||||
return (
|
||||
<div className="p-8 h-full flex flex-col items-center justify-center text-center animate-in fade-in zoom-in duration-500 max-w-2xl mx-auto">
|
||||
<div className="size-20 rounded-full bg-red-500/10 flex items-center justify-center mx-auto text-red-500 mb-6">
|
||||
<Lock size={40} />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold mb-4">Premium Feature</h2>
|
||||
<p className="text-muted-foreground text-lg mb-8">
|
||||
The Doubt Forum is a premium feature. Upgrade your plan to ask doubts and interact with tutors and the community.
|
||||
</p>
|
||||
<button
|
||||
className="px-8 py-3 bg-primary text-primary-foreground font-bold rounded-xl shadow-[0_0_20px_-5px_var(--color-primary)] text-lg"
|
||||
onClick={() => window.location.href = '/upgrade'}
|
||||
>
|
||||
Upgrade Plan
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
|
||||
@@ -88,17 +108,9 @@ export default function CommunityPage() {
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2 font-medium">Ask questions, share knowledge, and learn together with the community.</p>
|
||||
</div>
|
||||
|
||||
{hasAccessTo("ask-doubts-from-tutors") ? (
|
||||
<button className="px-5 py-2.5 bg-[#00F0FF] text-black font-bold rounded-xl shadow-[0_0_15px_-3px_rgba(0,240,255,0.4)] hover:bg-[#00d0dd] transition-colors">
|
||||
Ask a Doubt
|
||||
</button>
|
||||
) : (
|
||||
<button disabled className="px-5 py-2.5 bg-secondary text-muted-foreground font-bold rounded-xl flex items-center gap-2 cursor-not-allowed">
|
||||
<Lock size={16} />
|
||||
Pro Feature
|
||||
</button>
|
||||
)}
|
||||
<button className="px-5 py-2.5 bg-[#00F0FF] text-black font-bold rounded-xl shadow-[0_0_15px_-3px_rgba(0,240,255,0.4)] hover:bg-[#00d0dd] transition-colors">
|
||||
Ask a Doubt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Check, X, ShieldCheck, Loader2 } from "lucide-react"
|
||||
import { Check, X, ShieldCheck, Loader2, Minus } from "lucide-react"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
@@ -114,9 +114,9 @@ export default function UpgradePage() {
|
||||
<CardContent className="space-y-4 text-sm font-medium">
|
||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Subject-wise diagnostics</div>
|
||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Basic progress tracking</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Unlimited Tests</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> AI Spaced Repetition</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Parent Analytics</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Tests</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> AI Spaced Repetition</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Parent Analytics</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" className="w-full" disabled>Current Plan</Button>
|
||||
@@ -136,8 +136,8 @@ export default function UpgradePage() {
|
||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Unlimited Sectional Tests</div>
|
||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Performance Analytics</div>
|
||||
<div className="flex items-center gap-3"><Check className="text-green-500" size={18}/> Parent Dashboard</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> AI Mistake Heatmap</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><X size={18}/> Spaced Repetition (SM-2)</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> AI Mistake Heatmap</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground opacity-50"><Minus size={18}/> Spaced Repetition (SM-2)</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
|
||||
@@ -18,10 +18,11 @@ interface ExamAttemptPayload {
|
||||
|
||||
interface ExamResultProps {
|
||||
payload: ExamAttemptPayload;
|
||||
attemptId: string | null;
|
||||
onRetake: () => void;
|
||||
}
|
||||
|
||||
export function ExamResult({ payload, onRetake }: ExamResultProps) {
|
||||
export function ExamResult({ payload, attemptId, onRetake }: ExamResultProps) {
|
||||
const router = useRouter();
|
||||
const answered = payload.answers.filter(a => a.selected_answer !== null).length;
|
||||
const unanswered = payload.total_questions - answered;
|
||||
@@ -115,6 +116,25 @@ export function ExamResult({ payload, onRetake }: ExamResultProps) {
|
||||
<Button variant="outline" size="lg" className="rounded-xl h-14 px-8 font-bold text-lg gap-2" onClick={onRetake}>
|
||||
<RotateCcw className="size-5" /> Retake Mock Test
|
||||
</Button>
|
||||
{attemptId && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
className="rounded-xl h-14 px-8 font-bold text-lg bg-indigo-600 hover:bg-indigo-700 text-white gap-2 w-full sm:w-auto"
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
await document.exitFullscreen();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error exiting fullscreen", err);
|
||||
}
|
||||
router.push(`/dashboard/cbt/history/${attemptId}`);
|
||||
}}
|
||||
>
|
||||
View Solutions
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="lg"
|
||||
className="rounded-xl h-14 px-8 font-bold text-lg shadow-[0_0_20px_-5px_var(--color-primary)] gap-2 w-full sm:w-auto"
|
||||
|
||||
@@ -47,7 +47,8 @@ export interface ExamAttemptPayload {
|
||||
|
||||
interface CBTState {
|
||||
examId: string | null
|
||||
status: 'IN_PROGRESS' | 'SUBMITTED' | 'TIMED_OUT'
|
||||
attemptId: string | null
|
||||
status: 'NOT_STARTED' | 'IN_PROGRESS' | 'SUBMITTED' | 'TIMED_OUT'
|
||||
startTime: number | null
|
||||
totalDuration: number // in seconds
|
||||
questions: any[]
|
||||
@@ -56,6 +57,7 @@ interface CBTState {
|
||||
|
||||
// Actions
|
||||
initExam: (examId: string, questions: any[], duration: number) => void
|
||||
startExam: () => void
|
||||
selectAnswer: (questionId: string, answer: string) => void
|
||||
clearResponse: (questionId: string) => void
|
||||
markForReview: (questionId: string) => void
|
||||
@@ -63,7 +65,7 @@ interface CBTState {
|
||||
submitExam: (reason?: 'SUBMITTED' | 'TIMED_OUT') => void
|
||||
addTimeSpent: (questionId: string, seconds: number) => void
|
||||
getAttemptPayload: (userId: string) => ExamAttemptPayload | null
|
||||
submitAttemptToServer: (userId: string) => Promise<void>
|
||||
submitAttemptToServer: (userId: string) => Promise<string | null>
|
||||
resetExamState: () => void
|
||||
}
|
||||
|
||||
@@ -71,7 +73,8 @@ export const useCBTStore = create<CBTState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
examId: null,
|
||||
status: 'IN_PROGRESS',
|
||||
attemptId: null,
|
||||
status: 'NOT_STARTED',
|
||||
startTime: null,
|
||||
totalDuration: 0,
|
||||
questions: [],
|
||||
@@ -98,8 +101,9 @@ export const useCBTStore = create<CBTState>()(
|
||||
|
||||
set({
|
||||
examId,
|
||||
status: 'IN_PROGRESS',
|
||||
startTime: Date.now(), // Server-synced timestamp ideally
|
||||
attemptId: null,
|
||||
status: 'NOT_STARTED',
|
||||
startTime: null,
|
||||
totalDuration: duration,
|
||||
questions,
|
||||
answers: initialAnswers,
|
||||
@@ -111,6 +115,10 @@ export const useCBTStore = create<CBTState>()(
|
||||
}
|
||||
},
|
||||
|
||||
startExam: () => {
|
||||
set({ status: 'IN_PROGRESS', startTime: Date.now() })
|
||||
},
|
||||
|
||||
selectAnswer: (questionId, answer) => set((state) => {
|
||||
const current = state.answers[questionId]
|
||||
if (!current) return state;
|
||||
@@ -237,7 +245,7 @@ export const useCBTStore = create<CBTState>()(
|
||||
|
||||
submitAttemptToServer: async (userId: string) => {
|
||||
const payload = get().getAttemptPayload(userId);
|
||||
if (!payload) return;
|
||||
if (!payload) return null;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/attempts/submit`, {
|
||||
@@ -248,18 +256,24 @@ export const useCBTStore = create<CBTState>()(
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("Failed to submit attempt to server", await res.text());
|
||||
return null;
|
||||
} else {
|
||||
console.log("Successfully submitted attempt to server");
|
||||
const data = await res.json();
|
||||
console.log("Successfully submitted attempt to server", data.id);
|
||||
set({ attemptId: data.id });
|
||||
return data.id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Network error during submission:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
resetExamState: () => {
|
||||
set({
|
||||
examId: null,
|
||||
status: 'IN_PROGRESS',
|
||||
attemptId: null,
|
||||
status: 'NOT_STARTED',
|
||||
startTime: null,
|
||||
totalDuration: 0,
|
||||
questions: [],
|
||||
|
||||
18
test.js
Normal file
18
test.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
import("react-markdown").then(rm => {
|
||||
import("remark-math").then(math => {
|
||||
import("rehype-katex").then(katex => {
|
||||
import("react-dom/server").then(rds => {
|
||||
import("react").then(react => {
|
||||
const el = react.createElement(rm.default, {
|
||||
remarkPlugins: [math.default],
|
||||
rehypePlugins: [katex.default],
|
||||
children: "Testing math $$a=b$$ inline"
|
||||
});
|
||||
console.log(rds.renderToString(el));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
19
test2.js
Normal file
19
test2.js
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
import("react-markdown").then(rm => {
|
||||
import("remark-math").then(math => {
|
||||
import("rehype-katex").then(katex => {
|
||||
import("react-dom/server").then(rds => {
|
||||
import("react").then(react => {
|
||||
const text = "The velocity of a particle moving along the x-axis is given by $v(t) = \\int_{0}^{t} (3\\tau^2 - 2\\tau) d\\tau$. What is the displacement of the particle at $t = 2$ seconds if it starts from the origin?";
|
||||
const el = react.createElement(rm.default, {
|
||||
remarkPlugins: [math.default],
|
||||
rehypePlugins: [katex.default],
|
||||
children: text
|
||||
});
|
||||
console.log(rds.renderToString(el));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
19
test3.js
Normal file
19
test3.js
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
import("react-markdown").then(rm => {
|
||||
import("remark-math").then(math => {
|
||||
import("rehype-katex").then(katex => {
|
||||
import("react-dom/server").then(rds => {
|
||||
import("react").then(react => {
|
||||
const text = "The velocity of a particle moving along the x-axis is given by $\\v(t) = \\int_{0}^{t} (3\\tau^2 - 2\\tau) d\\tau$. What is the displacement of the particle at $t = 2$ seconds if it starts from the origin?";
|
||||
const el = react.createElement(rm.default, {
|
||||
remarkPlugins: [math.default],
|
||||
rehypePlugins: [katex.default],
|
||||
children: text
|
||||
});
|
||||
console.log(rds.renderToString(el));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
20
test4.js
Normal file
20
test4.js
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
import("react-markdown").then(rm => {
|
||||
import("remark-math").then(math => {
|
||||
import("rehype-katex").then(katex => {
|
||||
import("react-dom/server").then(rds => {
|
||||
import("react").then(react => {
|
||||
// This simulates string with actual backslash (e.g. from JSON parse)
|
||||
const text = "Testing math $\\v(t) = \\int$ and $$\\alpha$$";
|
||||
const el = react.createElement(rm.default, {
|
||||
remarkPlugins: [math.default],
|
||||
rehypePlugins: [katex.default],
|
||||
children: text
|
||||
});
|
||||
console.log(rds.renderToString(el));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user