Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions components/QuestionResponseTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";

type Question = {
id: number;
text: string;
type: "MCQ" | "MSQ";
inputtedAnswers: number[];
correctAnswers: number[];
options: { id: number; text: string }[];
};

interface Props {
questions: Question[];
}

export const QuestionResponseTable: React.FC<Props> = ({ questions }) => {
return (
<div className="space-y-2 max-h-72 overflow-y-auto">
{questions.map((question, idx) => (
<div key={idx} className="grid grid-cols-3 gap-2">
<div className="flex flex-col border rounded-md py-10 relative">
<div className="text-lg text-center px-2">{question.text}</div>
<div className="absolute bottom-2 right-2">
<span className="text-xs bg-[#EDEDED] rounded text-[#5C0505] px-2 py-1">
{question.type === "MCQ" ? "Multiple Choice" : "Multi-Select"}
</span>
</div>
</div>
<div className="border rounded-md text-lg flex items-center justify-center text-center">
{question.inputtedAnswers
.map((optId) => question.options.find((o) => o.id === optId)?.text)
.join(", ") || "—"}
</div>
<div className="border rounded-md text-lg flex items-center justify-center text-center">
{question.correctAnswers
.map((optId) => question.options.find((o) => o.id === optId)?.text)
.join(", ")}
</div>
</div>
))}
</div>
);
};
214 changes: 214 additions & 0 deletions components/StudentAnalyticsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
"use client";

import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { format } from "date-fns";
import { useEffect, useState } from "react";
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
import { QuestionResponseTable } from "@/components/QuestionResponseTable";
import { DatePicker } from "@/components/ui/DatePicker";
import DonutChart from "@/components/ui/DonutChart";
import { useToast } from "@/hooks/use-toast";
import {
studentAnalyticsAttendanceChartConfig,
studentAnalyticsScoreChartConfig,
} from "@/lib/constants";
import { getQuestionsAndResponsesForDate, getStudentAnalytics } from "@/services/analytics";

type Props = {
studentId: string | null;
courseId: number;
};

export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => {
const { toast } = useToast();
const [selectedDate, setSelectedDate] = useState(new Date());
const [analyticsData, setAnalyticsData] = useState<{
fullName: string;
attendancePercentage: number;
totalCheckIns: number;
lastCheckInDate: string | null;
mcqScore: number;
msqScore: number;
averagePollScore: number;
} | null>(null);

useEffect(() => {
if (!studentId) return;
getStudentAnalytics(courseId, studentId)
.then(setAnalyticsData)
.catch((err: unknown) => {
if (err instanceof Error) {
console.error("Failed to load analytics", err);
} else {
console.error("Unknown error occurred");
}
toast({
variant: "destructive",
title: "Error",
description: "Failed to load analytics",
});
});
}, [courseId, studentId]);

type QuestionForDate = {
id: number;
text: string;
type: "MCQ" | "MSQ";
inputtedAnswers: number[];
correctAnswers: number[];
options: { id: number; text: string }[];
};

const [questionsForDate, setQuestionsForDate] = useState<QuestionForDate[]>([]);

useEffect(() => {
const fetchQuestions = async () => {
if (!studentId) return;
const data = await getQuestionsAndResponsesForDate(courseId, studentId, selectedDate);
setQuestionsForDate(data);
};
void fetchQuestions();
}, [selectedDate]);

return (
<Sheet>
<SheetTrigger asChild>
<button className="w-32 h-8 bg-white border border-[#A5A5A5] hover:bg-slate-100 rounded-md">
View Activity →
</button>
</SheetTrigger>
<SheetContent className="w-[800px] max-w-full flex flex-col p-0">
<VisuallyHidden>
<SheetTitle>Student Analytics</SheetTitle>
</VisuallyHidden>

<div className="bg-[#F2F5FF] w-fit px-10 py-3 rounded-br-md border-b border-r">
<span className="text-primary text-lg">Student</span>
<div className="text-2xl">{analyticsData?.fullName ?? "Loading..."}</div>
</div>

<div className="px-10 py-3">
<span className="text-lg font-medium">Student&apos;s Performance</span>
<div className="p-4 rounded-md border flex justify-between">
<div className="flex justify-between items-center gap-4">
<div className="w-[200px] h-[200px]">
<DonutChart
chartData={[
{
name: "Correct",
value: analyticsData?.averagePollScore ?? 0,
fill: "#BFF2A7",
},
{
name: "Incorrect",
value: 100 - (analyticsData?.averagePollScore ?? 0),
fill: "#FFFFFF",
},
]}
chartConfig={studentAnalyticsScoreChartConfig}
dataKey="value"
nameKey="name"
description="Average Poll Score"
descriptionStatistic={analyticsData?.averagePollScore ?? 0}
/>
</div>
<div className="grid gap-y-4">
<div className="bg-[#E9FFDE] text-center px-4 py-2 rounded-md text-xs border shadow-lg">
Multiple Choice:
<div className="text-lg">
{analyticsData?.mcqScore ?? "--"}%
</div>
</div>
<div className="bg-[#E9FFDE] text-center px-4 py-2 rounded-md text-xs border shadow-lg">
Multi-Select:
<div className="text-lg">
{analyticsData?.msqScore ?? "--"}%
</div>
</div>
</div>
</div>
<div className="flex justify-between items-center gap-4">
<div className="w-[200px] h-[200px]">
<DonutChart
chartData={[
{
name: "Attended",
value: analyticsData?.attendancePercentage ?? 0,
fill: "#A7F2C2",
},
{
name: "Missed",
value: 100 - (analyticsData?.attendancePercentage ?? 0),
fill: "#FFFFFF",
},
]}
chartConfig={studentAnalyticsAttendanceChartConfig}
dataKey="value"
nameKey="name"
description="Attendance"
descriptionStatistic={analyticsData?.attendancePercentage ?? 0}
/>
</div>
<div className="grid gap-y-4">
<div className="text-center px-4 py-2 rounded-md text-xs border shadow-lg">
Last Check-in:
<div className="text-lg">
{analyticsData?.lastCheckInDate ?? "--"}
</div>
</div>
<div className="text-center px-4 py-2 rounded-md text-xs border shadow-lg">
Check-ins:
<div className="text-lg">
{analyticsData?.totalCheckIns ?? "--"}
</div>
</div>
</div>
</div>
</div>
</div>

<div className="px-10 flex justify-end">
<div className="w-fit">
<DatePicker
currentDate={selectedDate}
onSelect={(date: Date) => {
setSelectedDate(date);
}}
/>
</div>
</div>

<div className="flex px-10 gap-2 min-h-[334px]">
<div className="flex items-center gap-2">
<span className="text-sm">{format(selectedDate, "M/dd")}</span>
<div className="w-0.5 h-full bg-primary rounded-full"></div>
</div>

<div className="flex-1 space-y-2">
{questionsForDate.length === 0 ? (
<div className="flex items-center justify-center h-full border rounded-md bg-muted text-muted-foreground text-lg">
No questions for this day
</div>
) : (
<div className="flex-1 space-y-2">
<div className="grid grid-cols-3 gap-2">
<div className="border bg-[#F2F5FF] text-center rounded-md py-1 text-lg">
Question:
</div>
<div className="border bg-[#F2F5FF] text-center rounded-md py-1 text-lg">
Inputted:
</div>
<div className="border bg-[#F2F5FF] text-center rounded-md py-1 text-lg">
Correct Answer:
</div>
</div>

<QuestionResponseTable questions={questionsForDate} />
</div>
)}
</div>
</div>
</SheetContent>
</Sheet>
);
};
2 changes: 1 addition & 1 deletion components/ui/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Props {

export function DatePicker({ currentDate, onSelect }: Props) {
return (
<Popover>
<Popover modal={true}>
<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">
<p className="ml-3">{currentDate && format(currentDate, "PPP")}</p>
<CalendarIcon className="mx-3 h-4 w-4 float-end" />
Expand Down
2 changes: 2 additions & 0 deletions components/ui/DonutChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export default function DonutChart({
nameKey={nameKey}
innerRadius={"65%"}
strokeWidth={15}
startAngle={90}
endAngle={-270}
>
<Label
content={({ viewBox }) => {
Expand Down
9 changes: 6 additions & 3 deletions components/ui/StudentTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import { getStudents } from "@/services/userCourse";
import { getAllSessionIds } from "@/services/session";
import { getStudentsWithScores } from "@/lib/utils";
import LoaderComponent from "./loader";
import { StudentAnalyticsDrawer } from "../StudentAnalyticsDrawer";

interface Props {
courseId: number;
}
export default function StudentTable({ courseId }: Props) {
const [students, setStudents] = useState<
{
id: string;
name: string;
email: string | null;
attendance: number;
Expand Down Expand Up @@ -139,9 +141,10 @@ export default function StudentTable({ courseId }: Props) {
</TableCell>
<TableCell>
<div className="w-1/2 pr-6">
<button className="w-32 h-8 bg-white border border-[#A5A5A5] hover:bg-slate-100 rounded-md">
View Activity →
</button>
<StudentAnalyticsDrawer
courseId={courseId}
studentId={student.id}
/>
</div>
</TableCell>
</TableRow>
Expand Down
1 change: 1 addition & 0 deletions components/ui/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { signOut } from "next-auth/react";
export function AppSidebar() {
const pathname = usePathname();
const [isMenuOpen, setIsMenuOpen] = useState(false); // State to toggle dropdown menu
const [open, setOpen] = useState(false);

const links = [
{ name: "Dashboard", href: "/dashboard" },
Expand Down
10 changes: 10 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ export const attendanceChartConfig = {
},
} satisfies ChartConfig;

export const studentAnalyticsScoreChartConfig = {
Correct: { label: "Correct", color: "#BFF2A7" },
Incorrect: { label: "Incorrect", color: "#FFFFFF" },
} satisfies ChartConfig;

export const studentAnalyticsAttendanceChartConfig = {
Correct: { label: "Attended", color: "#A7F2C2" },
Incorrect: { label: "Missed", color: "#FFFFFF" },
} satisfies ChartConfig;

export const analyticsPages = ["Performance", "Attendance Rate"];
export const coursePages = ["Questionnaire", "Analytics"];

Expand Down
1 change: 1 addition & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export function getStudentsWithScores(students: Student[], sessionIds: number[])
: 0;

return {
id: student.id,
name: String(student.firstName) + " " + String(student.lastName),
email: student.email,
attendance,
Expand Down
Loading