Skip to content

Commit 340f69a

Browse files
Student analytics drawer (#51)
* Nearly complete UI * Added Donut Charts * fix question type location * Backend finished * Changed Donut Chart size * Replaced hardcoded values with props * Change date format * No Question for Date message added * Make donut start at the top and fill in towards the right. * added module that seems to have been removed * remove temporary button * remove comments that aren't necessary * lint fix * removed unused imports * added toast * fixed lint error about catch * remove apostrophe and put symbol value * Added question table component and removed isOpen variable * minor refactor * lint fix --------- Co-authored-by: Jerry <jerrythomasjohn9@gmail.com>
1 parent cb1067b commit 340f69a

11 files changed

Lines changed: 624 additions & 48 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from "react";
2+
3+
type Question = {
4+
id: number;
5+
text: string;
6+
type: "MCQ" | "MSQ";
7+
inputtedAnswers: number[];
8+
correctAnswers: number[];
9+
options: { id: number; text: string }[];
10+
};
11+
12+
interface Props {
13+
questions: Question[];
14+
}
15+
16+
export const QuestionResponseTable: React.FC<Props> = ({ questions }) => {
17+
return (
18+
<div className="space-y-2 max-h-72 overflow-y-auto">
19+
{questions.map((question, idx) => (
20+
<div key={idx} className="grid grid-cols-3 gap-2">
21+
<div className="flex flex-col border rounded-md py-10 relative">
22+
<div className="text-lg text-center px-2">{question.text}</div>
23+
<div className="absolute bottom-2 right-2">
24+
<span className="text-xs bg-[#EDEDED] rounded text-[#5C0505] px-2 py-1">
25+
{question.type === "MCQ" ? "Multiple Choice" : "Multi-Select"}
26+
</span>
27+
</div>
28+
</div>
29+
<div className="border rounded-md text-lg flex items-center justify-center text-center">
30+
{question.inputtedAnswers
31+
.map((optId) => question.options.find((o) => o.id === optId)?.text)
32+
.join(", ") || "—"}
33+
</div>
34+
<div className="border rounded-md text-lg flex items-center justify-center text-center">
35+
{question.correctAnswers
36+
.map((optId) => question.options.find((o) => o.id === optId)?.text)
37+
.join(", ")}
38+
</div>
39+
</div>
40+
))}
41+
</div>
42+
);
43+
};
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"use client";
2+
3+
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
4+
import { format } from "date-fns";
5+
import { useEffect, useState } from "react";
6+
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
7+
import { QuestionResponseTable } from "@/components/QuestionResponseTable";
8+
import { DatePicker } from "@/components/ui/DatePicker";
9+
import DonutChart from "@/components/ui/DonutChart";
10+
import { useToast } from "@/hooks/use-toast";
11+
import {
12+
studentAnalyticsAttendanceChartConfig,
13+
studentAnalyticsScoreChartConfig,
14+
} from "@/lib/constants";
15+
import { getQuestionsAndResponsesForDate, getStudentAnalytics } from "@/services/analytics";
16+
17+
type Props = {
18+
studentId: string | null;
19+
courseId: number;
20+
};
21+
22+
export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => {
23+
const { toast } = useToast();
24+
const [selectedDate, setSelectedDate] = useState(new Date());
25+
const [analyticsData, setAnalyticsData] = useState<{
26+
fullName: string;
27+
attendancePercentage: number;
28+
totalCheckIns: number;
29+
lastCheckInDate: string | null;
30+
mcqScore: number;
31+
msqScore: number;
32+
averagePollScore: number;
33+
} | null>(null);
34+
35+
useEffect(() => {
36+
if (!studentId) return;
37+
getStudentAnalytics(courseId, studentId)
38+
.then(setAnalyticsData)
39+
.catch((err: unknown) => {
40+
if (err instanceof Error) {
41+
console.error("Failed to load analytics", err);
42+
} else {
43+
console.error("Unknown error occurred");
44+
}
45+
toast({
46+
variant: "destructive",
47+
title: "Error",
48+
description: "Failed to load analytics",
49+
});
50+
});
51+
}, [courseId, studentId]);
52+
53+
type QuestionForDate = {
54+
id: number;
55+
text: string;
56+
type: "MCQ" | "MSQ";
57+
inputtedAnswers: number[];
58+
correctAnswers: number[];
59+
options: { id: number; text: string }[];
60+
};
61+
62+
const [questionsForDate, setQuestionsForDate] = useState<QuestionForDate[]>([]);
63+
64+
useEffect(() => {
65+
const fetchQuestions = async () => {
66+
if (!studentId) return;
67+
const data = await getQuestionsAndResponsesForDate(courseId, studentId, selectedDate);
68+
setQuestionsForDate(data);
69+
};
70+
void fetchQuestions();
71+
}, [selectedDate]);
72+
73+
return (
74+
<Sheet>
75+
<SheetTrigger asChild>
76+
<button className="w-32 h-8 bg-white border border-[#A5A5A5] hover:bg-slate-100 rounded-md">
77+
View Activity →
78+
</button>
79+
</SheetTrigger>
80+
<SheetContent className="w-[800px] max-w-full flex flex-col p-0">
81+
<VisuallyHidden>
82+
<SheetTitle>Student Analytics</SheetTitle>
83+
</VisuallyHidden>
84+
85+
<div className="bg-[#F2F5FF] w-fit px-10 py-3 rounded-br-md border-b border-r">
86+
<span className="text-primary text-lg">Student</span>
87+
<div className="text-2xl">{analyticsData?.fullName ?? "Loading..."}</div>
88+
</div>
89+
90+
<div className="px-10 py-3">
91+
<span className="text-lg font-medium">Student&apos;s Performance</span>
92+
<div className="p-4 rounded-md border flex justify-between">
93+
<div className="flex justify-between items-center gap-4">
94+
<div className="w-[200px] h-[200px]">
95+
<DonutChart
96+
chartData={[
97+
{
98+
name: "Correct",
99+
value: analyticsData?.averagePollScore ?? 0,
100+
fill: "#BFF2A7",
101+
},
102+
{
103+
name: "Incorrect",
104+
value: 100 - (analyticsData?.averagePollScore ?? 0),
105+
fill: "#FFFFFF",
106+
},
107+
]}
108+
chartConfig={studentAnalyticsScoreChartConfig}
109+
dataKey="value"
110+
nameKey="name"
111+
description="Average Poll Score"
112+
descriptionStatistic={analyticsData?.averagePollScore ?? 0}
113+
/>
114+
</div>
115+
<div className="grid gap-y-4">
116+
<div className="bg-[#E9FFDE] text-center px-4 py-2 rounded-md text-xs border shadow-lg">
117+
Multiple Choice:
118+
<div className="text-lg">
119+
{analyticsData?.mcqScore ?? "--"}%
120+
</div>
121+
</div>
122+
<div className="bg-[#E9FFDE] text-center px-4 py-2 rounded-md text-xs border shadow-lg">
123+
Multi-Select:
124+
<div className="text-lg">
125+
{analyticsData?.msqScore ?? "--"}%
126+
</div>
127+
</div>
128+
</div>
129+
</div>
130+
<div className="flex justify-between items-center gap-4">
131+
<div className="w-[200px] h-[200px]">
132+
<DonutChart
133+
chartData={[
134+
{
135+
name: "Attended",
136+
value: analyticsData?.attendancePercentage ?? 0,
137+
fill: "#A7F2C2",
138+
},
139+
{
140+
name: "Missed",
141+
value: 100 - (analyticsData?.attendancePercentage ?? 0),
142+
fill: "#FFFFFF",
143+
},
144+
]}
145+
chartConfig={studentAnalyticsAttendanceChartConfig}
146+
dataKey="value"
147+
nameKey="name"
148+
description="Attendance"
149+
descriptionStatistic={analyticsData?.attendancePercentage ?? 0}
150+
/>
151+
</div>
152+
<div className="grid gap-y-4">
153+
<div className="text-center px-4 py-2 rounded-md text-xs border shadow-lg">
154+
Last Check-in:
155+
<div className="text-lg">
156+
{analyticsData?.lastCheckInDate ?? "--"}
157+
</div>
158+
</div>
159+
<div className="text-center px-4 py-2 rounded-md text-xs border shadow-lg">
160+
Check-ins:
161+
<div className="text-lg">
162+
{analyticsData?.totalCheckIns ?? "--"}
163+
</div>
164+
</div>
165+
</div>
166+
</div>
167+
</div>
168+
</div>
169+
170+
<div className="px-10 flex justify-end">
171+
<div className="w-fit">
172+
<DatePicker
173+
currentDate={selectedDate}
174+
onSelect={(date: Date) => {
175+
setSelectedDate(date);
176+
}}
177+
/>
178+
</div>
179+
</div>
180+
181+
<div className="flex px-10 gap-2 min-h-[334px]">
182+
<div className="flex items-center gap-2">
183+
<span className="text-sm">{format(selectedDate, "M/dd")}</span>
184+
<div className="w-0.5 h-full bg-primary rounded-full"></div>
185+
</div>
186+
187+
<div className="flex-1 space-y-2">
188+
{questionsForDate.length === 0 ? (
189+
<div className="flex items-center justify-center h-full border rounded-md bg-muted text-muted-foreground text-lg">
190+
No questions for this day
191+
</div>
192+
) : (
193+
<div className="flex-1 space-y-2">
194+
<div className="grid grid-cols-3 gap-2">
195+
<div className="border bg-[#F2F5FF] text-center rounded-md py-1 text-lg">
196+
Question:
197+
</div>
198+
<div className="border bg-[#F2F5FF] text-center rounded-md py-1 text-lg">
199+
Inputted:
200+
</div>
201+
<div className="border bg-[#F2F5FF] text-center rounded-md py-1 text-lg">
202+
Correct Answer:
203+
</div>
204+
</div>
205+
206+
<QuestionResponseTable questions={questionsForDate} />
207+
</div>
208+
)}
209+
</div>
210+
</div>
211+
</SheetContent>
212+
</Sheet>
213+
);
214+
};

components/ui/DatePicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface Props {
1111

1212
export function DatePicker({ currentDate, onSelect }: Props) {
1313
return (
14-
<Popover>
14+
<Popover modal={true}>
1515
<PopoverTrigger className="h-11 w-full bg-[hsl(var(--secondary))] hover:bg-[hsl(var(--secondary))] text-black border border-slate-300 flex justify-between items-center font-normal shadow-none rounded-lg">
1616
<p className="ml-3">{currentDate && format(currentDate, "PPP")}</p>
1717
<CalendarIcon className="mx-3 h-4 w-4 float-end" />

components/ui/DonutChart.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export default function DonutChart({
3434
nameKey={nameKey}
3535
innerRadius={"65%"}
3636
strokeWidth={15}
37+
startAngle={90}
38+
endAngle={-270}
3739
>
3840
<Label
3941
content={({ viewBox }) => {

components/ui/StudentTable.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import { getStudents } from "@/services/userCourse";
1313
import { getAllSessionIds } from "@/services/session";
1414
import { getStudentsWithScores } from "@/lib/utils";
1515
import LoaderComponent from "./loader";
16+
import { StudentAnalyticsDrawer } from "../StudentAnalyticsDrawer";
1617

1718
interface Props {
1819
courseId: number;
1920
}
2021
export default function StudentTable({ courseId }: Props) {
2122
const [students, setStudents] = useState<
2223
{
24+
id: string;
2325
name: string;
2426
email: string | null;
2527
attendance: number;
@@ -139,9 +141,10 @@ export default function StudentTable({ courseId }: Props) {
139141
</TableCell>
140142
<TableCell>
141143
<div className="w-1/2 pr-6">
142-
<button className="w-32 h-8 bg-white border border-[#A5A5A5] hover:bg-slate-100 rounded-md">
143-
View Activity →
144-
</button>
144+
<StudentAnalyticsDrawer
145+
courseId={courseId}
146+
studentId={student.id}
147+
/>
145148
</div>
146149
</TableCell>
147150
</TableRow>

components/ui/app-sidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { signOut } from "next-auth/react";
1717
export function AppSidebar() {
1818
const pathname = usePathname();
1919
const [isMenuOpen, setIsMenuOpen] = useState(false); // State to toggle dropdown menu
20+
const [open, setOpen] = useState(false);
2021

2122
const links = [
2223
{ name: "Dashboard", href: "/dashboard" },

lib/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ export const attendanceChartConfig = {
4949
},
5050
} satisfies ChartConfig;
5151

52+
export const studentAnalyticsScoreChartConfig = {
53+
Correct: { label: "Correct", color: "#BFF2A7" },
54+
Incorrect: { label: "Incorrect", color: "#FFFFFF" },
55+
} satisfies ChartConfig;
56+
57+
export const studentAnalyticsAttendanceChartConfig = {
58+
Correct: { label: "Attended", color: "#A7F2C2" },
59+
Incorrect: { label: "Missed", color: "#FFFFFF" },
60+
} satisfies ChartConfig;
61+
5262
export const analyticsPages = ["Performance", "Attendance Rate"];
5363
export const coursePages = ["Questionnaire", "Analytics"];
5464

lib/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export function getStudentsWithScores(students: Student[], sessionIds: number[])
127127
: 0;
128128

129129
return {
130+
id: student.id,
130131
name: String(student.firstName) + " " + String(student.lastName),
131132
email: student.email,
132133
attendance,

0 commit comments

Comments
 (0)