feat: complete UI tasks, backend optimizations, coupon admin, user kundli

This commit is contained in:
2026-07-10 12:36:56 +05:30
parent 149d365218
commit d5199682e3
16 changed files with 782 additions and 78 deletions

View File

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