Gateprep / frontend /src /pages /TestEngine.jsx
banu4prasad's picture
add: ShadcnUI and fix mobile layout
eac6aad
Raw
History Blame Contribute Delete
12 kB
import AlertTriangle from 'lucide-react/dist/esm/icons/alert-triangle'
import Maximize from 'lucide-react/dist/esm/icons/maximize'
import Spinner from '../components/shared/Spinner'
import Calculator from '../components/test/Calculator'
import QuestionView from '../components/test/QuestionView'
import TestHeader from '../components/test/TestHeader'
import TestSidebar from '../components/test/TestSidebar'
import TimerDisplay from '../components/test/TimerDisplay'
import useTestEngine from '../hooks/useTestEngine'
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
function LoadingScreen() {
return (
<div className="min-h-screen flex items-center justify-center theme-surface">
<Spinner size={36} className="text-sky-500" />
</div>
)
}
function InstructionScreen({
accepted,
beginTest,
navigate,
setAccepted,
starting,
test,
}) {
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-background">
<Card className="w-full max-w-2xl animate-fade-in border-border">
<CardHeader className="flex flex-row items-start justify-between gap-4">
<div>
<CardTitle className="text-2xl font-bold mb-1">General Instructions</CardTitle>
<p className="text-sm text-muted-foreground">{test?.title}</p>
</div>
<Button variant="ghost" size="sm" onClick={() => navigate('/tests')}>
Back
</Button>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-6">
{[
['Duration', `${test?.duration_minutes || 0} min`],
['Questions', test?.question_count || 0],
['Marks', test?.total_marks || 0],
].map(([label, value]) => (
<div key={label} className="bg-muted rounded border p-3 text-center">
<p className="text-xs mb-1 text-muted-foreground">{label}</p>
<p className="font-semibold text-foreground">{value}</p>
</div>
))}
</div>
<div className="space-y-3 text-sm mb-6 text-muted-foreground">
<p>The test opens in fullscreen mode after you click Begin Test.</p>
<p>Leaving fullscreen or switching tabs may be counted as a violation.</p>
<p>The timer starts only after the attempt is created.</p>
</div>
<label className="flex items-start gap-3 mb-6 cursor-pointer">
<input
type="checkbox"
checked={accepted}
onChange={event => setAccepted(event.target.checked)}
className="mt-1"
/>
<span className="text-sm text-foreground">
I have read and understood the instructions.
</span>
</label>
<Button
onClick={beginTest}
disabled={!accepted || starting}
className="w-full h-11 text-base font-medium"
>
{starting ? <Spinner size={18} className="mr-2" /> : <Maximize size={18} className="mr-2" />}
{starting ? 'Starting Test...' : 'Begin Test'}
</Button>
</CardContent>
</Card>
</div>
)
}
function EmptyQuestionsScreen({ navigate }) {
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-background">
<Card className="w-full max-w-md text-center border-border">
<CardContent className="p-6 pt-8">
<AlertTriangle size={36} className="text-amber-400 mx-auto mb-3" />
<h1 className="text-xl font-bold mb-2 text-foreground">No Questions Available</h1>
<p className="text-sm mb-5 text-muted-foreground">
This test does not have any questions to display.
</p>
<Button onClick={() => navigate('/tests')} className="w-full">
Back to Tests
</Button>
</CardContent>
</Card>
</div>
)
}
function FullscreenWarning({
endTimeMs,
fsViolations,
handleTimerExpire,
setShowConfirm,
setShowFsWarning,
}) {
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center theme-danger-backdrop">
<div className="text-center max-w-sm p-8 rounded-xl border border-red-500/40 theme-card-bg">
<AlertTriangle size={44} className="text-red-400 mx-auto mb-4" />
<h2 className="text-xl font-bold mb-2 theme-text">You exited fullscreen!</h2>
<p className="text-sm mb-1 theme-muted">The timer is still running.</p>
<p className="text-red-400 font-semibold mb-1">Violation {fsViolations}/3</p>
<p className="text-xs mb-5 theme-muted">3 violations = test auto-submitted</p>
<TimerDisplay
endTime={endTimeMs}
onExpire={handleTimerExpire}
announceWarnings={false}
className="text-3xl font-mono font-bold mb-6"
lowClassName="text-red-400 timer-critical"
normalClassName="theme-text"
/>
<div className="space-y-3">
<button
onClick={() => {
document.documentElement.requestFullscreen?.()
.then(() => setShowFsWarning(false))
.catch(() => setShowFsWarning(false))
}}
className="btn-primary w-full flex items-center justify-center gap-2"
>
<Maximize size={16} /> Return to Fullscreen
</button>
<button
onClick={() => {
setShowFsWarning(false)
setShowConfirm(true)
}}
className="btn-danger w-full"
>
Submit Test Now
</button>
</div>
</div>
</div>
)
}
function ConfirmSubmitModal({
answered,
doSubmit,
marked,
notAnswered,
questions,
setShowConfirm,
submitting,
}) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 theme-modal-backdrop">
<div
className="w-full max-w-sm p-6 rounded-xl animate-slide-up theme-card-surface"
role="dialog"
aria-modal="true"
aria-labelledby="confirm-submit-title"
>
<div className="flex items-center gap-3 mb-4">
<AlertTriangle size={22} className="text-amber-400" />
<h3 id="confirm-submit-title" className="font-bold text-lg theme-text">Submit Test?</h3>
</div>
<div className="space-y-1.5 mb-5 text-sm">
{[
['Total Questions', questions.length, 'theme-text'],
['Answered', answered, 'text-[var(--success-text)]'],
['Not Answered', notAnswered, 'text-[var(--danger-text)]'],
['Marked', marked.size, 'text-[var(--marked-text)]'],
].map(([label, val, colorClass]) => (
<div key={label} className="flex justify-between py-1.5 border-b theme-border">
<span className="theme-muted">{label}</span>
<span className={`font-semibold ${colorClass}`}>{val}</span>
</div>
))}
</div>
<p className="text-xs mb-5 theme-muted">
This cannot be undone. Your answers will be evaluated.
</p>
<div className="flex gap-3">
<button onClick={() => setShowConfirm(false)} className="btn-ghost flex-1">Go Back</button>
<button
onClick={() => {
setShowConfirm(false)
doSubmit(false)
}}
disabled={submitting}
className="btn-primary flex-1 flex items-center justify-center gap-2"
>
{submitting && <Spinner size={13} />} Submit
</button>
</div>
</div>
</div>
)
}
export default function TestEngine() {
const engine = useTestEngine()
const {
accepted,
answered,
answers,
attemptNumber,
beginTest,
clearResponse,
commitNAT,
current,
currentQuestion,
doSubmit,
endTimeMs,
fsViolations,
handleTimerExpire,
loading,
markAndNext,
marked,
maxAttempts,
natInput,
navigate,
notAnswered,
questions,
saveAndNext,
setAccepted,
setCurrent,
setMCQ,
setNatInput,
setShowCalc,
setShowConfirm,
setShowFsWarning,
setShowPalette,
showCalc,
showConfirm,
showFsWarning,
showPalette,
started,
starting,
subjects,
submitting,
test,
toggleMSQ,
totalViolations,
visited,
} = engine
if (loading) return <LoadingScreen />
if (!started) {
return (
<InstructionScreen
accepted={accepted}
beginTest={beginTest}
navigate={navigate}
setAccepted={setAccepted}
starting={starting}
test={test}
/>
)
}
if (!currentQuestion) return <EmptyQuestionsScreen navigate={navigate} />
const sidebar = (
<TestSidebar
answered={answered}
answers={answers}
currentQuestion={currentQuestion}
marked={marked}
notAnswered={notAnswered}
onQuestionSelect={(index) => {
setCurrent(index)
setShowPalette(false)
}}
onSubmitClick={() => setShowConfirm(true)}
questions={questions}
submitting={submitting}
subjects={subjects}
visited={visited}
/>
)
return (
<div className="theme-surface h-screen flex flex-col overflow-hidden select-none">
{showFsWarning && (
<FullscreenWarning
endTimeMs={endTimeMs}
fsViolations={fsViolations}
handleTimerExpire={handleTimerExpire}
setShowConfirm={setShowConfirm}
setShowFsWarning={setShowFsWarning}
/>
)}
<TestHeader
attemptNumber={attemptNumber}
currentQuestion={currentQuestion}
endTimeMs={endTimeMs}
handleTimerExpire={handleTimerExpire}
maxAttempts={maxAttempts}
onSubmitClick={() => setShowConfirm(true)}
questions={questions}
setCurrent={setCurrent}
setShowCalc={setShowCalc}
showCalc={showCalc}
subjects={subjects}
submitting={submitting}
test={test}
totalViolations={totalViolations}
/>
<div className="flex flex-1 overflow-hidden">
<QuestionView
answers={answers}
clearResponse={clearResponse}
commitNAT={commitNAT}
current={current}
markAndNext={markAndNext}
natInput={natInput}
question={currentQuestion}
questions={questions}
saveAndNext={saveAndNext}
setCurrent={setCurrent}
setMCQ={setMCQ}
setNatInput={setNatInput}
toggleMSQ={toggleMSQ}
/>
<div className="hidden md:flex w-52 border-l flex-col flex-shrink-0 overflow-y-auto theme-sidebar-surface">
{sidebar}
</div>
<Sheet open={showPalette} onOpenChange={setShowPalette}>
<SheetTrigger asChild>
<Button
className="md:hidden fixed bottom-16 right-4 z-30 flex items-center gap-1.5 rounded-full shadow-lg"
size="sm"
>
Q{current + 1}/{questions.length}
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[85vw] sm:w-80 p-0 overflow-y-auto">
<SheetHeader className="p-3 border-b text-left px-4">
<SheetTitle className="text-sm">Question Palette</SheetTitle>
</SheetHeader>
{sidebar}
</SheetContent>
</Sheet>
</div>
{showCalc && <Calculator onClose={() => setShowCalc(false)} />}
{showConfirm && (
<ConfirmSubmitModal
answered={answered}
doSubmit={doSubmit}
marked={marked}
notAnswered={notAnswered}
questions={questions}
setShowConfirm={setShowConfirm}
submitting={submitting}
/>
)}
</div>
)
}