Fix UI issues, timer logic, theme switcher, view solutions, and markdown rendering

This commit is contained in:
2026-07-09 16:08:12 +05:30
parent bc444ea737
commit b80d73ec26
18 changed files with 1662 additions and 41 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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