feat: complete UI tasks, backend optimizations, coupon admin, user kundli
This commit is contained in:
@@ -37,6 +37,7 @@ 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 [accessError, setAccessError] = useState<string | null>(null);
|
||||
const [localTheme, setLocalTheme] = useState<string>('light');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,6 +98,20 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
const testRes = await fetch(`${apiUrl}/api/tests/${examId}?user_id=b0e17378-864d-439e-8806-708dc48a891b`);
|
||||
if (!testRes.ok) throw new Error("Failed to fetch test structure");
|
||||
const testData = await testRes.json();
|
||||
|
||||
// Strict boundary checking
|
||||
const now = new Date();
|
||||
if (testData.scheduled_date && new Date(testData.scheduled_date) > now) {
|
||||
setAccessError(`This exam is scheduled for ${new Date(testData.scheduled_date).toLocaleString()}. You cannot start it yet.`);
|
||||
setMounted(true);
|
||||
return;
|
||||
}
|
||||
if (testData.end_date && new Date(testData.end_date) < now) {
|
||||
setAccessError(`This exam expired on ${new Date(testData.end_date).toLocaleString()} and is no longer available.`);
|
||||
setMounted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setExamType(testData.type);
|
||||
|
||||
// Extract all question IDs from sections
|
||||
@@ -232,7 +247,26 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
await submitAttemptToServer('b0e17378-864d-439e-8806-708dc48a891b');
|
||||
}
|
||||
|
||||
if (!mounted || questions.length === 0) return <div className="p-8">Loading Exam Engine...</div>;
|
||||
if (!mounted || questions.length === 0) {
|
||||
return <div className="min-h-screen flex items-center justify-center bg-background"><div className="animate-pulse flex items-center gap-2"><Clock className="animate-spin text-muted-foreground" size={20}/><span className="font-bold text-muted-foreground tracking-widest uppercase text-sm">Initializing CBT Engine</span></div></div>
|
||||
}
|
||||
|
||||
if (accessError) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
||||
<div className="max-w-md w-full bg-secondary/20 p-8 rounded-2xl border border-border text-center space-y-4">
|
||||
<div className="mx-auto size-12 bg-red-500/10 text-red-500 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle size={24} />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold">Access Denied</h2>
|
||||
<p className="text-muted-foreground font-medium">{accessError}</p>
|
||||
<Button variant="outline" className="mt-4" onClick={() => window.history.back()}>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'NOT_STARTED') {
|
||||
return (
|
||||
@@ -293,8 +327,18 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
const currentQuestion = questions[currentQuestionIndex];
|
||||
const currentState = currentQuestion ? answers[currentQuestion.id] : null
|
||||
|
||||
// Helper to ensure math options missing $ delimiters get rendered properly
|
||||
const renderLatex = (text: string) => {
|
||||
if (typeof text !== 'string') return text;
|
||||
// If it contains backslash but no dollar signs, we might want to wrap the whole thing in $ $
|
||||
if (text.includes('\\') && !text.includes('$')) {
|
||||
return `$${text}$`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const handleOptionSelect = (qId: string, letter: string, type: string) => {
|
||||
if (type === 'Multiple_Choice') {
|
||||
if (type === 'MCQ_Multiple' || type === 'Multiple_Choice') {
|
||||
const currentSelected = currentState?.selected_answer || "";
|
||||
const selectedArray = currentSelected.split(',').filter(Boolean);
|
||||
if (selectedArray.includes(letter)) {
|
||||
@@ -421,6 +465,13 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
{currentQuestion.text_en}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
{(currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice') && (
|
||||
<p className="text-sm font-bold text-muted-foreground mt-6 mb-[-1rem] bg-secondary/50 p-2.5 rounded-md border border-border inline-flex items-center gap-2">
|
||||
<AlertTriangle size={16} className="text-amber-500" />
|
||||
This question has MULTIPLE CORRECT OPTIONS. Please select all that apply.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3 mt-8">
|
||||
{currentQuestion.type === 'Numerical' ? (
|
||||
@@ -441,7 +492,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
) : (
|
||||
currentQuestion.options?.map((option: string, idx: number) => {
|
||||
const letter = String.fromCharCode(65 + idx);
|
||||
const isSelected = currentQuestion.type === 'Multiple_Choice'
|
||||
const isMultiple = currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice';
|
||||
const isSelected = isMultiple
|
||||
? (currentState?.selected_answer || "").split(',').includes(letter)
|
||||
: currentState?.selected_answer === letter;
|
||||
|
||||
@@ -453,14 +505,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'}
|
||||
`}
|
||||
>
|
||||
<div className={`flex-shrink-0 size-8 ${currentQuestion.type === 'Multiple_Choice' ? 'rounded-md' : 'rounded-full'} border flex items-center justify-center font-medium transition-colors
|
||||
<div className={`flex-shrink-0 size-8 ${isMultiple ? 'rounded-md' : 'rounded-full'} border flex items-center justify-center font-medium transition-colors
|
||||
${isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5'}
|
||||
`}>
|
||||
{letter}
|
||||
</div>
|
||||
<span className={`text-base font-semibold flex-1 ${isSelected ? 'text-primary' : ''}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex, rehypeRaw]}>
|
||||
{option}
|
||||
{renderLatex(option)}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</button>
|
||||
@@ -481,6 +533,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground bg-secondary/30 px-2 py-1 rounded-md">+4 / -1</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => markForReview(currentQuestion.id)}
|
||||
className={`h-8 w-8 rounded-lg transition-colors ${currentState?.status.includes('MARKED') ? 'text-amber-500 bg-amber-500/10' : 'text-muted-foreground hover:text-amber-500'}`}
|
||||
>
|
||||
<Flag size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose prose-neutral dark:prose-invert max-w-none text-lg leading-relaxed text-foreground font-medium">
|
||||
@@ -488,6 +548,13 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
{currentQuestion.text_hi}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
{(currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice') && (
|
||||
<p className="text-sm font-bold text-muted-foreground mt-6 mb-[-1rem] bg-secondary/50 p-2.5 rounded-md border border-border inline-flex items-center gap-2">
|
||||
<AlertTriangle size={16} className="text-amber-500" />
|
||||
This question has MULTIPLE CORRECT OPTIONS. Please select all that apply.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3 mt-8">
|
||||
{currentQuestion.type === 'Numerical' ? (
|
||||
@@ -508,7 +575,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
) : (
|
||||
currentQuestion.options_hi?.map((option: string, idx: number) => {
|
||||
const letter = String.fromCharCode(65 + idx);
|
||||
const isSelected = currentQuestion.type === 'Multiple_Choice'
|
||||
const isMultiple = currentQuestion.type === 'MCQ_Multiple' || currentQuestion.type === 'Multiple_Choice';
|
||||
const isSelected = isMultiple
|
||||
? (currentState?.selected_answer || "").split(',').includes(letter)
|
||||
: currentState?.selected_answer === letter;
|
||||
|
||||
@@ -520,14 +588,14 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
${isSelected ? 'border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]' : 'border-border bg-background hover:border-primary/50 hover:bg-secondary/20'}
|
||||
`}
|
||||
>
|
||||
<div className={`flex-shrink-0 size-8 ${currentQuestion.type === 'Multiple_Choice' ? 'rounded-md' : 'rounded-full'} border flex items-center justify-center font-medium transition-colors
|
||||
<div className={`flex-shrink-0 size-8 ${isMultiple ? 'rounded-md' : 'rounded-full'} border flex items-center justify-center font-medium transition-colors
|
||||
${isSelected ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary group-hover:bg-primary/5'}
|
||||
`}>
|
||||
{letter}
|
||||
</div>
|
||||
<span className={`text-base font-semibold flex-1 ${isSelected ? 'text-primary' : ''}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex, rehypeRaw]}>
|
||||
{option}
|
||||
{renderLatex(option)}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user