UI/UX Polishing: CBT dashboard, sidebar tiers, landing pricing, and exam disclaimer
This commit is contained in:
@@ -32,6 +32,7 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
const resolvedParams = use(params);
|
||||
const examId = resolvedParams.examId;
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [examType, setExamType] = useState<string>('Test');
|
||||
const [showCalc, setShowCalc] = useState(false);
|
||||
const [showScratchpad, setShowScratchpad] = useState(false);
|
||||
const [showPeriodicTable, setShowPeriodicTable] = useState(false);
|
||||
@@ -61,7 +62,10 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
submitAttemptToServer,
|
||||
resetExamState,
|
||||
status,
|
||||
attemptId
|
||||
attemptId,
|
||||
currentQuestionId,
|
||||
getQuestion,
|
||||
examState
|
||||
} = useCBTStore();
|
||||
|
||||
const { formattedTime, isTimedOut } = useExamTimer();
|
||||
@@ -75,6 +79,7 @@ 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();
|
||||
setExamType(testData.type);
|
||||
|
||||
// Extract all question IDs from sections
|
||||
const questionIds: string[] = [];
|
||||
@@ -82,23 +87,31 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
questionIds.push(...section.questions);
|
||||
});
|
||||
|
||||
// Fetch full question objects
|
||||
const fetchedQuestions = await Promise.all(
|
||||
questionIds.map(async (id, idx) => {
|
||||
const qRes = await fetch(`${apiUrl}/api/questions/${id}`);
|
||||
const qData = await qRes.json();
|
||||
return {
|
||||
id: qData.id,
|
||||
number: idx + 1,
|
||||
subject: qData.subject,
|
||||
type: qData.type,
|
||||
text_en: qData.question_text,
|
||||
text_hi: qData.question_text_hi || qData.question_text,
|
||||
options: Object.values(qData.options || {}),
|
||||
options_hi: Object.values(qData.options_hi || qData.options || {}),
|
||||
};
|
||||
})
|
||||
);
|
||||
const qRes = await fetch(`${apiUrl}/api/tests/${examId}/questions`);
|
||||
if (!qRes.ok) throw new Error("Failed to fetch questions in bulk");
|
||||
const allQuestionsData = await qRes.json();
|
||||
|
||||
// Map questions by ID for fast lookup
|
||||
const questionsMap = new Map();
|
||||
allQuestionsData.forEach((q: any) => {
|
||||
questionsMap.set(q.id, q);
|
||||
});
|
||||
|
||||
// Construct fetchedQuestions retaining the exact order of questionIds
|
||||
const fetchedQuestions = questionIds.map((id, idx) => {
|
||||
const qData = questionsMap.get(id);
|
||||
if (!qData) return null; // Fallback in case of mismatch
|
||||
return {
|
||||
id: qData.id,
|
||||
number: idx + 1,
|
||||
subject: qData.subject,
|
||||
type: qData.type,
|
||||
text_en: qData.question_text,
|
||||
text_hi: qData.question_text_hi || qData.question_text,
|
||||
options: Object.values(qData.options || {}),
|
||||
options_hi: Object.values(qData.options_hi || qData.options || {}),
|
||||
};
|
||||
}).filter(Boolean) as any[];
|
||||
|
||||
initExam(examId, fetchedQuestions, testData.total_time * 60);
|
||||
setMounted(true);
|
||||
@@ -221,6 +234,10 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-xl text-yellow-600 dark:text-yellow-500 text-sm text-left">
|
||||
<strong>Disclaimer:</strong> Only the score obtained in your <em>first attempt</em> will be considered for growth tracking and performance evaluation. Any subsequent attempts are intended solely for practice and will not be included in the official assessment.
|
||||
</div>
|
||||
|
||||
<div className="mb-8 p-4 bg-muted rounded-xl flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-semibold">Preferred Theme</p>
|
||||
@@ -247,19 +264,34 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
<ExamResult
|
||||
payload={payload}
|
||||
attemptId={attemptId}
|
||||
onRetake={() => {
|
||||
onRetake={examType === 'DPS' ? () => {
|
||||
resetExamState();
|
||||
window.location.reload();
|
||||
}}
|
||||
} : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const currentQuestion = questions[currentQuestionIndex];
|
||||
const currentState = answers[currentQuestion.id];
|
||||
const currentQuestion = getQuestion(currentQuestionId)
|
||||
const currentState = currentQuestion ? examState?.answers[currentQuestion.id] : null
|
||||
|
||||
// Derive palette stats
|
||||
const paletteStats = Object.values(answers).reduce(
|
||||
const handleOptionSelect = (qId: string, letter: string, type: string) => {
|
||||
if (type === 'Multiple_Choice') {
|
||||
const currentSelected = currentState?.selected_answer || "";
|
||||
const selectedArray = currentSelected.split(',').filter(Boolean);
|
||||
if (selectedArray.includes(letter)) {
|
||||
const newArray = selectedArray.filter((l: string) => l !== letter).sort();
|
||||
selectAnswer(qId, newArray.length > 0 ? newArray.join(',') : null);
|
||||
} else {
|
||||
selectedArray.push(letter);
|
||||
selectAnswer(qId, selectedArray.sort().join(','));
|
||||
}
|
||||
} else {
|
||||
selectAnswer(qId, letter);
|
||||
}
|
||||
}
|
||||
|
||||
// Effect to close overlay tools on EscapealetteStats = Object.values(answers).reduce(
|
||||
(acc, val) => {
|
||||
acc[val.status]++;
|
||||
return acc;
|
||||
@@ -322,9 +354,9 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
|
||||
<div className="flex items-center gap-3 md:gap-6">
|
||||
<div className="hidden lg:flex items-center gap-2 mr-4">
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowCalc(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><CalculatorIcon size={14}/> Calc</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowPeriodicTable(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><FlaskConical size={14}/> Periodic Table</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowScratchpad(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:text-foreground"><Edit3 size={14}/> Scratchpad</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowCalc(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:bg-[#00F0FF] hover:text-black transition-colors"><CalculatorIcon size={14}/> Calculator</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowPeriodicTable(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:bg-[#00F0FF] hover:text-black transition-colors"><FlaskConical size={14}/> Periodic Table</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowScratchpad(true)} className="gap-2 text-xs font-semibold text-muted-foreground hover:bg-[#00F0FF] hover:text-black transition-colors"><Edit3 size={14}/> Scratchpad</Button>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center gap-2 px-3 py-1.5 md:px-4 md:py-2 rounded-xl font-mono text-base md:text-lg tracking-wider font-medium border border-border ${isTimedOut ? 'bg-red-500/10 text-red-500' : 'bg-secondary/50 text-foreground'}`}>
|
||||
@@ -368,7 +400,15 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mt-8">
|
||||
{(currentQuestion.type === 'Subjective' || currentQuestion.type === 'Numerical') ? (
|
||||
{currentQuestion.type === 'Numerical' ? (
|
||||
<input
|
||||
type="text"
|
||||
className="w-full p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent text-base"
|
||||
placeholder="Type your numerical answer here..."
|
||||
value={currentState?.selected_answer || ""}
|
||||
onChange={(e) => selectAnswer(currentQuestion.id, e.target.value)}
|
||||
/>
|
||||
) : currentQuestion.type === 'Subjective' ? (
|
||||
<textarea
|
||||
className="w-full min-h-[150px] p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent resize-y text-base"
|
||||
placeholder="Type your answer or explanation here..."
|
||||
@@ -378,16 +418,19 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
) : (
|
||||
currentQuestion.options?.map((option: string, idx: number) => {
|
||||
const letter = String.fromCharCode(65 + idx);
|
||||
const isSelected = currentState?.selected_answer === letter;
|
||||
const isSelected = currentQuestion.type === 'Multiple_Choice'
|
||||
? (currentState?.selected_answer || "").split(',').includes(letter)
|
||||
: currentState?.selected_answer === letter;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => selectAnswer(currentQuestion.id, letter)}
|
||||
onClick={() => handleOptionSelect(currentQuestion.id, letter, currentQuestion.type)}
|
||||
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
|
||||
${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 rounded-lg border flex items-center justify-center font-medium transition-colors
|
||||
<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
|
||||
${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}
|
||||
@@ -424,7 +467,15 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mt-8">
|
||||
{(currentQuestion.type === 'Subjective' || currentQuestion.type === 'Numerical') ? (
|
||||
{currentQuestion.type === 'Numerical' ? (
|
||||
<input
|
||||
type="text"
|
||||
className="w-full p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent text-base"
|
||||
placeholder="अपना संख्यात्मक उत्तर यहाँ टाइप करें..."
|
||||
value={currentState?.selected_answer || ""}
|
||||
onChange={(e) => selectAnswer(currentQuestion.id, e.target.value)}
|
||||
/>
|
||||
) : currentQuestion.type === 'Subjective' ? (
|
||||
<textarea
|
||||
className="w-full min-h-[150px] p-4 rounded-xl border border-border bg-background focus:ring-2 focus:ring-primary focus:border-transparent resize-y text-base"
|
||||
placeholder="अपना उत्तर या स्पष्टीकरण यहाँ टाइप करें..."
|
||||
@@ -434,16 +485,19 @@ 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 = currentState?.selected_answer === letter;
|
||||
const isSelected = currentQuestion.type === 'Multiple_Choice'
|
||||
? (currentState?.selected_answer || "").split(',').includes(letter)
|
||||
: currentState?.selected_answer === letter;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => selectAnswer(currentQuestion.id, letter)}
|
||||
onClick={() => handleOptionSelect(currentQuestion.id, letter, currentQuestion.type)}
|
||||
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent
|
||||
${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 rounded-lg border flex items-center justify-center font-medium transition-colors
|
||||
<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
|
||||
${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}
|
||||
|
||||
Reference in New Issue
Block a user