Skip to content

Commit 494919a

Browse files
Quiz on practiceproblem page (#889)
* Added a dropdown for quiz in practice problem page Added a new dropdown for showing quizes in the practice problem page , and also added a drop down to show in which quiz a question apeeard , this is in persectionquiz page * minor * minor-change * minor/2 * cleanup * minor change --------- Co-authored-by: khushbu-25 <khushigupta2572025@gmail.com>
1 parent 094e25e commit 494919a

File tree

8 files changed

+493
-22
lines changed

8 files changed

+493
-22
lines changed

packages/alea-frontend/components/ProblemList.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getExamsForCourse } from '@alea/spec';
1919
import { ExamSelect } from '@alea/stex-react-renderer';
2020
import { useCourseProblemCounts } from '../hooks/useCourseProblemCount';
2121
import shadows from '../theme/shadows';
22+
import { getQuizzesForCourse } from '@alea/spec';
2223

2324
interface TitleMetadata {
2425
uri?: string;
@@ -72,6 +73,9 @@ const sortExamsByDateDesc = (exams: ExamInfo[]): ExamInfo[] => {
7273
const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
7374
const [exams, setExams] = useState<ExamInfo[]>([]);
7475
const [selectedExam, setSelectedExam] = useState('');
76+
77+
const [quizzes, setQuizzes] = useState<ExamInfo[]>([]);
78+
const [selectedQuiz, setSelectedQuiz] = useState('');
7579
const router = useRouter();
7680
const { practiceProblems: t, peerGrading: g } = getLocaleObject(router);
7781
const theme = useTheme();
@@ -87,6 +91,18 @@ const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
8791
.catch(console.error);
8892
}, [courseId]);
8993

94+
useEffect(() => {
95+
if (!courseId) return;
96+
97+
getQuizzesForCourse(courseId)
98+
.then((data) => {
99+
console.log("QUIZ DATA:", data);
100+
const sorted = sortExamsByDateDesc(data);
101+
setQuizzes(sorted);
102+
})
103+
.catch(console.error);
104+
}, [courseId]);
105+
90106
const { data: problemCounts = {}, isLoading } = useCourseProblemCounts(courseId);
91107

92108
if (isLoading) {
@@ -166,7 +182,7 @@ const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
166182
bgcolor: 'bacground.paper',
167183
borderRadius: '12px',
168184
border: '1px solid ',
169-
borderColor:'divider',
185+
borderColor: 'divider',
170186
boxShadow: shadows[2],
171187
}}
172188
>
@@ -200,6 +216,21 @@ const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
200216
}}
201217
label="Select Exam"
202218
/>
219+
220+
<ExamSelect
221+
exams={quizzes}
222+
courseId={courseId}
223+
value={selectedQuiz}
224+
onChange={(quizUri) => {
225+
setSelectedQuiz(quizUri);
226+
227+
router.push({
228+
pathname: '/quiz-problems',
229+
query: { quizUri, courseId },
230+
});
231+
}}
232+
label="Select Quiz"
233+
/>
203234
</Box>
204235
</Box>
205236

@@ -320,4 +351,4 @@ const ProblemList: FC<ProblemListProps> = ({ courseSections, courseId }) => {
320351
);
321352
};
322353

323-
export default ProblemList;
354+
export default ProblemList;

packages/alea-frontend/pages/api/get-problems-per-section.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
22
import { getAllCoursesFromDb } from './get-all-courses';
33
import { getCategorizedProblems } from './get-categorized-problem';
4-
import { getExamsForCourse, getProblemsForExam } from '@alea/spec';
4+
import {
5+
getExamsForCourse,
6+
getProblemsForExam,
7+
getProblemsForQuiz,
8+
getQuizzesForCourse,
9+
} from '@alea/spec';
510
import { Language } from '@alea/utils';
611

712
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -30,11 +35,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
3035

3136
const sectionProblemSet = new Set(practiceProblems.map((p) => p.problemId));
3237

38+
const quizzes = await getQuizzesForCourse(courseId);
39+
const quizProblemMap = new Map<string, { quizUri: string; quizLabel: string }[]>();
40+
3341
for (const exam of exams) {
3442
const examProblems = await getProblemsForExam(exam.uri);
3543

3644
for (const problemUri of examProblems) {
37-
3845
if (!sectionProblemSet.has(problemUri)) continue;
3946

4047
examOnlyProblemSet.add(problemUri);
@@ -49,6 +56,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
4956
}
5057
}
5158

59+
for (const quiz of quizzes) {
60+
const quizProblems = await getProblemsForQuiz(quiz.uri);
61+
62+
for (const problemUri of quizProblems) {
63+
if (!sectionProblemSet.has(problemUri)) continue;
64+
65+
const existing = quizProblemMap.get(problemUri) ?? [];
66+
67+
existing.push({
68+
quizUri: quiz.uri,
69+
quizLabel: quiz.number ? `Quiz ${quiz.number}` : 'Quiz',
70+
});
71+
72+
quizProblemMap.set(problemUri, existing);
73+
}
74+
}
75+
5276
const practiceProblemSet = new Set(practiceProblems.map((p) => p.problemId));
5377

5478
const examOnlyProblems = Array.from(examOnlyProblemSet)
@@ -64,6 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
6488
const enrichedProblems = allProblems.map((p) => ({
6589
...p,
6690
examRefs: examProblemMap.get(p.problemId) ?? [],
91+
quizRefs: quizProblemMap.get(p.problemId) ?? [],
6792
}));
6893

6994
return res.status(200).json(enrichedProblems);
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { useEffect, useMemo, useState } from 'react';
2+
import { useRouter } from 'next/router';
3+
import { Box, Chip, CircularProgress, Tooltip, Typography } from '@mui/material';
4+
5+
import {
6+
FTMLProblemWithSolution,
7+
getProblemsForQuiz,
8+
formatQuizLabelShortFromUri,
9+
formatQuizLabelFullFromUri,
10+
getQuizMetadataByUri,
11+
} from '@alea/spec';
12+
13+
import {
14+
AnswerContext,
15+
GradingContext,
16+
QuizDisplay,
17+
ShowGradingFor,
18+
} from '@alea/stex-react-renderer';
19+
20+
import MainLayout from '../layouts/MainLayout';
21+
import { contentFragment } from '@flexiformal/ftml-backend';
22+
23+
async function buildFTMLProblem(problemUri: string): Promise<FTMLProblemWithSolution> {
24+
const fragmentResponse: any[] = await contentFragment({ uri: problemUri });
25+
return {
26+
problem: {
27+
uri: problemUri,
28+
html: fragmentResponse[2],
29+
title_html: '',
30+
},
31+
answerClasses: [],
32+
};
33+
}
34+
35+
async function buildQuizProblems(
36+
problemUris: string[]
37+
): Promise<Record<string, FTMLProblemWithSolution>> {
38+
const result: Record<string, FTMLProblemWithSolution> = {};
39+
await Promise.all(
40+
problemUris.map(async (uri) => {
41+
result[uri] = await buildFTMLProblem(uri);
42+
})
43+
);
44+
return result;
45+
}
46+
47+
const QuizProblemsPage = () => {
48+
const router = useRouter();
49+
const quizUri = router.query.quizUri as string | undefined;
50+
const targetProblemId = router.query.problemId as string | undefined;
51+
52+
const [quizMeta, setQuizMeta] = useState<any>(null);
53+
const [problems, setProblems] = useState<Record<string, FTMLProblemWithSolution>>({});
54+
const [loading, setLoading] = useState(true);
55+
const [initialIndex, setInitialIndex] = useState<number>(0);
56+
57+
useEffect(() => {
58+
if (!quizUri) return;
59+
60+
const fetchData = async () => {
61+
setLoading(true);
62+
try {
63+
const decodedUri = decodeURIComponent(quizUri);
64+
65+
const meta = await getQuizMetadataByUri(decodedUri);
66+
setQuizMeta(meta);
67+
68+
const uris = await getProblemsForQuiz(decodedUri);
69+
70+
if (targetProblemId) {
71+
const idx = uris.indexOf(decodeURIComponent(targetProblemId));
72+
if (idx !== -1) setInitialIndex(idx);
73+
}
74+
75+
const quizProblems = await buildQuizProblems(uris);
76+
setProblems(quizProblems);
77+
} catch (error) {
78+
console.error('Error loading quiz data:', error);
79+
} finally {
80+
setLoading(false);
81+
}
82+
};
83+
84+
fetchData();
85+
}, [quizUri, targetProblemId]);
86+
87+
const quizLabelShort = useMemo(() => {
88+
if (!quizUri || !quizMeta) return '';
89+
return formatQuizLabelShortFromUri(quizUri, quizMeta);
90+
}, [quizUri, quizMeta]);
91+
92+
const quizLabelFull = useMemo(() => {
93+
if (!quizUri || !quizMeta) return '';
94+
return formatQuizLabelFullFromUri(quizUri, quizMeta);
95+
}, [quizUri, quizMeta]);
96+
97+
if (loading) {
98+
return (
99+
<MainLayout title="Quiz">
100+
<Box display="flex" justifyContent="center" alignItems="center" height="80vh">
101+
<CircularProgress />
102+
</Box>
103+
</MainLayout>
104+
);
105+
}
106+
107+
return (
108+
<MainLayout title={`Review: ${quizLabelFull}`}>
109+
<Box sx={{ px: 2, pt: 2 }}>
110+
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 1 }}>
111+
{quizLabelShort && (
112+
<Tooltip
113+
title={
114+
<Box>
115+
<Typography variant="subtitle2" fontWeight="bold">
116+
{quizLabelFull}
117+
</Typography>
118+
<Typography variant="caption" color="inherit">
119+
This problem belongs to this quiz
120+
</Typography>
121+
</Box>
122+
}
123+
placement="left"
124+
arrow
125+
>
126+
<Chip
127+
label={quizLabelShort}
128+
color="primary"
129+
sx={{
130+
fontWeight: 600,
131+
px: 1.5,
132+
borderRadius: '8px',
133+
boxShadow: '0px 3px 10px rgba(25,118,210,0.3)',
134+
background: 'linear-gradient(90deg, #1976d2 0%, #42a5f5 100%)',
135+
}}
136+
/>
137+
</Tooltip>
138+
)}
139+
</Box>
140+
141+
<GradingContext.Provider
142+
value={{
143+
showGradingFor: ShowGradingFor.INSTRUCTOR,
144+
isGrading: false,
145+
showGrading: false,
146+
gradingInfo: undefined,
147+
studentId: undefined,
148+
}}
149+
>
150+
<AnswerContext.Provider value={{}}>
151+
<QuizDisplay
152+
problems={problems}
153+
existingResponses={{}}
154+
isFrozen={false}
155+
showPerProblemTime={false}
156+
isExamProblem={false}
157+
initialProblemIdx={initialIndex}
158+
/>
159+
</AnswerContext.Provider>
160+
</GradingContext.Provider>
161+
</Box>
162+
</MainLayout>
163+
);
164+
};
165+
166+
export default QuizProblemsPage;

packages/stex-react-renderer/src/lib/ExamSelect.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ export function ExamSelect({
2525
label = 'Appeared in exams',
2626
size = 'small',
2727
}: ExamSelectProps) {
28-
if (!exams.length) return null;
29-
3028
return (
3129
<FormControl size={size} sx={{ minWidth: 180 }}>
3230
<InputLabel sx={{ fontSize: '0.85rem' }}>{label}</InputLabel>
@@ -45,7 +43,7 @@ export function ExamSelect({
4543
}}
4644
>
4745
<MenuItem disabled value="">
48-
<em>Select exam</em>
46+
<em>{exams.length ? 'Select' : 'No items available'}</em>
4947
</MenuItem>
5048

5149
{exams.map((exam) => {

0 commit comments

Comments
 (0)