From 18475acafe580d9ada76dbf530f4edf991be9419 Mon Sep 17 00:00:00 2001 From: janayagarcia Date: Tue, 20 May 2025 14:06:15 -0700 Subject: [PATCH 01/20] Nearly complete UI --- components/StudentAnalyticsDrawer.tsx | 138 ++++++++++++++++++++++++++ components/ui/app-sidebar.tsx | 6 ++ package-lock.json | 132 +++++++++++++++++++++++- package.json | 1 + 4 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 components/StudentAnalyticsDrawer.tsx diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx new file mode 100644 index 0000000..c47c790 --- /dev/null +++ b/components/StudentAnalyticsDrawer.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { + Sheet, + SheetContent, + SheetTrigger, + SheetTitle, +} from "./ui/sheet"; +import { Button } from "@/components/ui/button"; +import { CalendarIcon } from "lucide-react"; +import { useState } from "react"; +import { format } from "date-fns"; +import { Input } from "./ui/input"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; +import { DatePicker } from "@/components/ui/DatePicker"; + + +export const StudentAnalyticsDrawer = () => { + const [isOpen, setIsOpen] = useState(false); + const [selectedDate, setSelectedDate] = useState(new Date()); + + return ( + + + + + + + Student Analytics + + + {/* Student Name */} +
+ Student +
Kim Taehyung
+
+ + {/* Performance Summary */} +
+ Student's Performance +
+
+
+
Average Poll Score
+
85
+
+
+
+ Multiple Choice: +
85%
+
+
+ Multi-Select: +
85%
+
+
+
+
+
+
Attendance
+
90
+
+
+
+ Last Check-in: +
1/16
+
+
+ Check-ins: +
5
+
+
+
+
+
+ + {/* Date Selector */} +
+
+ { + setSelectedDate(date); + }} + /> +
+
+ +
+ {/* Div A: Date + Vertical Line */} +
+ 1/13 +
+
+ + {/* Div B: Questions + Answers */} +
+ {/* Div 1: Header Row */} +
+
Question:
+
Inputted:
+
Correct Answer:
+
+ + {/* Div 2: Content Rows */} +
+ {[...Array(5)].map((_, idx) => ( +
+ {/* Question */} +
+
+ Who is Ash's partner in Pokémon? +
+
+
+ Multi-Select +
+
+
+ + {/* Inputted */} +
+ Pikachu +
+ + {/* Correct */} +
+ Pikachu +
+
+ ))} +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/components/ui/app-sidebar.tsx b/components/ui/app-sidebar.tsx index 6c1c9b1..6298b0e 100644 --- a/components/ui/app-sidebar.tsx +++ b/components/ui/app-sidebar.tsx @@ -13,10 +13,12 @@ import { import { cn } from "@/lib/utils"; import { Button } from "./button"; import { signOut } from "next-auth/react"; +import { StudentAnalyticsDrawer } from "@/components/StudentAnalyticsDrawer" 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" }, @@ -104,6 +106,10 @@ export function AppSidebar() { ))} +
+ + {open && } +
- {open && } + {open && } - {open && } - - - - - Student Analytics - + return ( + + + + + + + Student Analytics + -
- Student -
{analyticsData?.fullName ?? "Loading..."}
-
+
+ Student +
{analyticsData?.fullName ?? "Loading..."}
+
-
- Student's Performance -
-
-
- -
-
-
- Multiple Choice: -
{analyticsData?.mcqScore ?? "--"}%
+
+ Student's Performance +
+
+
+ +
+
+
+ Multiple Choice: +
+ {analyticsData?.mcqScore ?? "--"}% +
+
+
+ Multi-Select: +
+ {analyticsData?.msqScore ?? "--"}% +
+
+
-
- Multi-Select: -
{analyticsData?.msqScore ?? "--"}%
+
+
+ +
+
+
+ Last Check-in: +
+ {analyticsData?.lastCheckInDate ?? "--"} +
+
+
+ Check-ins: +
+ {analyticsData?.totalCheckIns ?? "--"} +
+
+
-
-
- +
+ { + setSelectedDate(date); + }} />
-
-
- Last Check-in: -
{analyticsData?.lastCheckInDate ?? "--"}
-
-
- Check-ins: -
{analyticsData?.totalCheckIns ?? "--"}
-
-
-
-
- -
-
- { - setSelectedDate(date); - }} - /> -
-
-
-
- - {format(selectedDate, "M/dd")} - -
-
- -
- {questionsForDate.length === 0 ? ( -
- No questions for this day +
+
+ {format(selectedDate, "M/dd")} +
- ) : ( -
-
-
Question:
-
Inputted:
-
Correct Answer:
-
-
- {questionsForDate.map((question, idx) => ( -
-
-
{question.text}
-
- - {question.type === "MCQ" ? "Multiple Choice" : "Multi-Select"} - -
-
-
- {question.inputtedAnswers - .map((optId) => question.options.find(o => o.id === optId)?.text) - .join(", ") || "—"} +
+ {questionsForDate.length === 0 ? ( +
+ No questions for this day +
+ ) : ( +
+
+
+ Question: +
+
+ Inputted: +
+
+ Correct Answer: +
-
- {question.correctAnswers - .map((optId) => question.options.find(o => o.id === optId)?.text) - .join(", ")} + +
+ {questionsForDate.map((question, idx) => ( +
+
+
+ {question.text} +
+
+ + {question.type === "MCQ" + ? "Multiple Choice" + : "Multi-Select"} + +
+
+
+ {question.inputtedAnswers + .map( + (optId) => + question.options.find( + (o) => o.id === optId, + )?.text, + ) + .join(", ") || "—"} +
+
+ {question.correctAnswers + .map( + (optId) => + question.options.find( + (o) => o.id === optId, + )?.text, + ) + .join(", ")} +
+
+ ))}
- ))} -
+ )}
- )} -
-
- - - ); -}; \ No newline at end of file +
+ + + ); +}; diff --git a/components/ui/CourseCard.tsx b/components/ui/CourseCard.tsx index a3ab2d5..0ff6e98 100644 --- a/components/ui/CourseCard.tsx +++ b/components/ui/CourseCard.tsx @@ -5,187 +5,186 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { MoreVertical, Edit, Trash2 } from "lucide-react"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AddEditCourseForm } from "@/components/AddEditCourseForm"; import { dayLabels } from "@/lib/constants"; export type CourseCardProps = { - color: string; - days: string[]; - title: string; - timeStart: string; - timeEnd: string; - code: string; - role: Role; - id: number; - onEdit?: () => void; - onDelete?: () => void; + color: string; + days: string[]; + title: string; + timeStart: string; + timeEnd: string; + code: string; + role: Role; + id: number; + onEdit?: () => void; + onDelete?: () => void; }; export default function CourseCard({ - color, - days, - title, - timeStart, - timeEnd, - code, - role, - id, - onEdit, - onDelete, + color, + days, + title, + timeStart, + timeEnd, + code, + role, + id, + onEdit, + onDelete, }: CourseCardProps) { - const router = useRouter(); - const [isEditOpen, setIsEditOpen] = useState(false); + const router = useRouter(); + const [isEditOpen, setIsEditOpen] = useState(false); - const shortDays = days - .map((fullDay) => { - const entry = Object.entries(dayLabels).find(([, label]) => label === fullDay); - return entry ? entry[0] : undefined; - }) - .filter(Boolean) as ("M" | "T" | "W" | "Th" | "F")[]; + const shortDays = days + .map((fullDay) => { + const entry = Object.entries(dayLabels).find(([, label]) => label === fullDay); + return entry ? entry[0] : undefined; + }) + .filter(Boolean) as ("M" | "T" | "W" | "Th" | "F")[]; - const handleCardClick = () => { - if (!isEditOpen) { - router.push( - role === "LECTURER" - ? `/dashboard/course/${id}/questionnaire` - : `/dashboard/course/${id}/live-poll` - ); - } - }; + const handleCardClick = () => { + if (!isEditOpen) { + router.push( + role === "LECTURER" + ? `/dashboard/course/${id}/questionnaire` + : `/dashboard/course/${id}/live-poll`, + ); + } + }; - const handleEdit = (e: React.MouseEvent) => { - e.stopPropagation(); - setIsEditOpen(true); - }; + const handleEdit = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsEditOpen(true); + }; - const handleDelete = (e: React.MouseEvent) => { - e.stopPropagation(); - onDelete?.(); - }; + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + onDelete?.(); + }; - const handleEditClose = () => { - setIsEditOpen(false); - onEdit?.(); - }; + const handleEditClose = () => { + setIsEditOpen(false); + onEdit?.(); + }; - const CardContent = () => ( - <> -
-
-

- Time: {timeStart} - {timeEnd} -

-

{title}

- -

- Code:{" "} - - {(+code).toLocaleString("en-US", { - minimumIntegerDigits: 6, - useGrouping: false, - })} - -

-

- Role: {role.toLocaleLowerCase()} -

-
-

{days.join(", ")}

-
- - ); + const CardContent = () => ( + <> +
+
+

+ Time: {timeStart} - {timeEnd} +

+

{title}

+ +

+ Code:{" "} + + {(+code).toLocaleString("en-US", { + minimumIntegerDigits: 6, + useGrouping: false, + })} + +

+

+ Role: {role.toLocaleLowerCase()} +

+
+

{days.join(", ")}

+
+ + ); - const MobileCardContent = () => ( - <> -
-
-

- Time: {timeStart} - {timeEnd} -

-

{title}

-
-

{days.join(", ")}

- - ); + const MobileCardContent = () => ( + <> +
+
+

+ Time: {timeStart} - {timeEnd} +

+

{title}

+
+

{days.join(", ")}

+ + ); - const MenuDropdown = () => ( - - - - - - {role === "LECTURER" && ( - { - event.preventDefault(); - handleEdit(event as unknown as React.MouseEvent); - }} - > - - Edit - - )} - handleDelete(event as unknown as React.MouseEvent)}> - - {role === "LECTURER" ? "Delete" : "Leave"} - - - - ); + const MenuDropdown = () => ( + + + + + + {role === "LECTURER" && ( + { + event.preventDefault(); + handleEdit(event as unknown as React.MouseEvent); + }} + > + + Edit + + )} + handleDelete(event as unknown as React.MouseEvent)} + > + + {role === "LECTURER" ? "Delete" : "Leave"} + + + + ); - return ( - <> - {/* Mobile View */} -
- - -
+ return ( + <> + {/* Mobile View */} +
+ + +
- {/* Desktop View */} -
- - -
+ {/* Desktop View */} +
+ + +
- !open && handleEditClose()} - defaultValues={{ - title, - color, - days: shortDays, - startTime: timeStart, - endTime: timeEnd, - }} - onSuccess={handleEditClose} - /> - - ); -} \ No newline at end of file + !open && handleEditClose()} + defaultValues={{ + title, + color, + days: shortDays, + startTime: timeStart, + endTime: timeEnd, + }} + onSuccess={handleEditClose} + /> + + ); +} diff --git a/components/ui/DonutChart.tsx b/components/ui/DonutChart.tsx index f3b55a5..c20ed0d 100644 --- a/components/ui/DonutChart.tsx +++ b/components/ui/DonutChart.tsx @@ -70,4 +70,4 @@ export default function DonutChart({ ); -} \ No newline at end of file +} diff --git a/components/ui/SlidingCalendar.tsx b/components/ui/SlidingCalendar.tsx index f5252ef..82aa56c 100644 --- a/components/ui/SlidingCalendar.tsx +++ b/components/ui/SlidingCalendar.tsx @@ -253,7 +253,11 @@ function SlidingCalendar({ courseId, refreshTrigger }: Props) { courseId={courseId} location="page" questionId={question.id} - onUpdate={() => fetchQuestions(selectedDate.toDate())} + onUpdate={() => + fetchQuestions( + selectedDate.toDate(), + ) + } prevData={{ question: question.text, selectedQuestionType: diff --git a/components/ui/answerOptions.tsx b/components/ui/answerOptions.tsx index ecd56f8..9467497 100644 --- a/components/ui/answerOptions.tsx +++ b/components/ui/answerOptions.tsx @@ -16,7 +16,6 @@ interface AnswerOptionsProps { onSelectionChange: (value: number | number[]) => void; } - const AnswerOptions: React.FC = ({ options, questionType, diff --git a/lib/utils.ts b/lib/utils.ts index 012c43a..14677e9 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -32,7 +32,6 @@ export function formatDateToISO(date: Date) { return new Date(date.setHours(0, 0, 0, 0)).toISOString(); } - export function shuffleArray(array: T[]): T[] { const copy = [...array]; for (let i = copy.length - 1; i > 0; i--) { diff --git a/services/analytics.ts b/services/analytics.ts index 02a12ca..831b40f 100644 --- a/services/analytics.ts +++ b/services/analytics.ts @@ -1,127 +1,139 @@ "use server"; -import prisma from "@/lib/prisma"; import dayjs from "dayjs"; +import prisma from "@/lib/prisma"; export async function getStudentAnalytics(courseId: number, userId: string) { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { firstName: true, lastName: true }, - }); + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { firstName: true, lastName: true }, + }); - if (!user) throw new Error("User not found"); + if (!user) throw new Error("User not found"); - const fullName = `${user.firstName} ${user.lastName ?? ""}`.trim(); + const fullName = `${user.firstName} ${user.lastName ?? ""}`.trim(); - const sessions = await prisma.courseSession.findMany({ - where: { courseId }, - include: { - questions: { + const sessions = await prisma.courseSession.findMany({ + where: { courseId }, include: { - options: true, - responses: { - where: { userId }, - }, + questions: { + include: { + options: true, + responses: { + where: { userId }, + }, + }, + }, }, - }, - }, - }); - - const sessionDates = sessions.map((s) => dayjs(s.startTime).format("YYYY-MM-DD")); - const allDatesSet = new Set(sessionDates); - - const checkInDatesSet = new Set(); - let mcqTotal = 0, mcqCorrect = 0; - let msqTotal = 0, msqCorrect = 0; - - for (const session of sessions) { - let studentAnswered = false; - for (const question of session.questions) { - const isMCQ = question.type === "MCQ"; - const isMSQ = question.type === "MSQ"; - - const correctOptions = question.options.filter((o) => o.isCorrect); - const studentResponses = question.responses; - - if (studentResponses.length > 0) { - studentAnswered = true; - if (isMCQ) { - mcqTotal++; - if (studentResponses[0]?.optionId === correctOptions[0]?.id) { - mcqCorrect++; - } - } else if (isMSQ) { - msqTotal++; - const selected = new Set(studentResponses.map((r) => r.optionId)); - const expected = new Set(correctOptions.map((o) => o.id)); - - const matched = - selected.size === expected.size && - [...selected].every((id) => expected.has(id)); - - if (matched) msqCorrect++; + }); + + const sessionDates = sessions.map((s) => dayjs(s.startTime).format("YYYY-MM-DD")); + const allDatesSet = new Set(sessionDates); + + const checkInDatesSet = new Set(); + let mcqTotal = 0, + mcqCorrect = 0; + let msqTotal = 0, + msqCorrect = 0; + + for (const session of sessions) { + let studentAnswered = false; + for (const question of session.questions) { + const isMCQ = question.type === "MCQ"; + const isMSQ = question.type === "MSQ"; + + const correctOptions = question.options.filter((o) => o.isCorrect); + const studentResponses = question.responses; + + if (studentResponses.length > 0) { + studentAnswered = true; + if (isMCQ) { + mcqTotal++; + if (studentResponses[0]?.optionId === correctOptions[0]?.id) { + mcqCorrect++; + } + } else if (isMSQ) { + msqTotal++; + const selected = new Set(studentResponses.map((r) => r.optionId)); + const expected = new Set(correctOptions.map((o) => o.id)); + + const matched = + selected.size === expected.size && + [...selected].every((id) => expected.has(id)); + + if (matched) msqCorrect++; + } + } + } + if (studentAnswered) { + checkInDatesSet.add(dayjs(session.startTime).format("MM/DD")); } - } - } - if (studentAnswered) { - checkInDatesSet.add(dayjs(session.startTime).format("MM/DD")); } - } - - const totalSessions = allDatesSet.size; - const attendedSessions = checkInDatesSet.size; - const lastCheckIn = [...checkInDatesSet].sort().pop(); - - const mcqScore = mcqTotal ? Math.round((mcqCorrect / mcqTotal) * 100) : 0; - const msqScore = msqTotal ? Math.round((msqCorrect / msqTotal) * 100) : 0; - const averagePollScore = Math.round((mcqScore + msqScore) / 2); - - return { - fullName, - attendancePercentage: totalSessions ? Math.round((attendedSessions / totalSessions) * 100) : 0, - totalCheckIns: attendedSessions, - lastCheckInDate: lastCheckIn ?? null, - mcqScore, - msqScore, - averagePollScore, - }; + + const totalSessions = allDatesSet.size; + const attendedSessions = checkInDatesSet.size; + const lastCheckIn = [...checkInDatesSet].sort().pop(); + + const mcqScore = mcqTotal ? Math.round((mcqCorrect / mcqTotal) * 100) : 0; + const msqScore = msqTotal ? Math.round((msqCorrect / msqTotal) * 100) : 0; + const averagePollScore = Math.round((mcqScore + msqScore) / 2); + + return { + fullName, + attendancePercentage: totalSessions + ? Math.round((attendedSessions / totalSessions) * 100) + : 0, + totalCheckIns: attendedSessions, + lastCheckInDate: lastCheckIn ?? null, + mcqScore, + msqScore, + averagePollScore, + }; } -export async function getQuestionsAndResponsesForDate(courseId: number, studentId: string, date: Date) { +export async function getQuestionsAndResponsesForDate( + courseId: number, + studentId: string, + date: Date, +) { try { - const sessions = await prisma.courseSession.findMany({ - where: { - courseId, - startTime: { - gte: new Date(date.setHours(0, 0, 0, 0)), - lt: new Date(date.setHours(23, 59, 59, 999)), - }, - }, - include: { - questions: { + const sessions = await prisma.courseSession.findMany({ + where: { + courseId, + startTime: { + gte: new Date(date.setHours(0, 0, 0, 0)), + lt: new Date(date.setHours(23, 59, 59, 999)), + }, + }, include: { - options: true, - responses: { - where: { - userId: studentId, + questions: { + include: { + options: true, + responses: { + where: { + userId: studentId, + }, + }, + }, }, - }, }, - }, - }, - }); - - const questions = sessions.flatMap(session => session.questions).map(question => ({ - id: question.id, - text: question.text, - type: question.type, - inputtedAnswers: question.responses.map(r => r.optionId), - correctAnswers: question.options.filter(opt => opt.isCorrect).map(opt => opt.id), - options: question.options.map(opt => ({ id: opt.id, text: opt.text })), - })); - - return questions; + }); + + const questions = sessions + .flatMap((session) => session.questions) + .map((question) => ({ + id: question.id, + text: question.text, + type: question.type, + inputtedAnswers: question.responses.map((r) => r.optionId), + correctAnswers: question.options + .filter((opt) => opt.isCorrect) + .map((opt) => opt.id), + options: question.options.map((opt) => ({ id: opt.id, text: opt.text })), + })); + + return questions; } catch (error) { - console.error("Failed to get questions and responses:", error); - return []; + console.error("Failed to get questions and responses:", error); + return []; } - } \ No newline at end of file +} diff --git a/services/course.ts b/services/course.ts index 544a326..9ef88b4 100644 --- a/services/course.ts +++ b/services/course.ts @@ -115,7 +115,7 @@ type UpdateCourseParams = { export async function updateCourse( courseId: number, - data: UpdateCourseParams + data: UpdateCourseParams, ): Promise { try { // First update the course details @@ -148,10 +148,8 @@ export async function updateCourse( return updatedCourse; } catch (error) { console.error("Error updating course:", error); - return { - error: error instanceof Error - ? error.message - : "Failed to update course" + return { + error: error instanceof Error ? error.message : "Failed to update course", }; } -} \ No newline at end of file +} From 221f46d98716c53d9c20e62168c59762ab9307c5 Mon Sep 17 00:00:00 2001 From: janayagarcia Date: Wed, 28 May 2025 14:14:30 -0700 Subject: [PATCH 14/20] removed unused imports --- components/StudentAnalyticsDrawer.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx index 9055901..744a90f 100644 --- a/components/StudentAnalyticsDrawer.tsx +++ b/components/StudentAnalyticsDrawer.tsx @@ -2,9 +2,7 @@ import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { format } from "date-fns"; -import { CalendarIcon } from "lucide-react"; import { useEffect, useState } from "react"; -import { Input } from "./ui/input"; import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet"; import { DatePicker } from "@/components/ui/DatePicker"; import DonutChart from "@/components/ui/DonutChart"; From e47921eb155559508a1e12252168c654dc8df2ce Mon Sep 17 00:00:00 2001 From: janayagarcia Date: Wed, 28 May 2025 14:18:17 -0700 Subject: [PATCH 15/20] added toast --- components/StudentAnalyticsDrawer.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx index 744a90f..ef95bd0 100644 --- a/components/StudentAnalyticsDrawer.tsx +++ b/components/StudentAnalyticsDrawer.tsx @@ -8,6 +8,8 @@ import { DatePicker } from "@/components/ui/DatePicker"; import DonutChart from "@/components/ui/DonutChart"; import { Button } from "@/components/ui/button"; import { getQuestionsAndResponsesForDate, getStudentAnalytics } from "@/services/analytics"; +import { useToast } from "@/hooks/use-toast"; + type Props = { studentId: string; @@ -15,6 +17,7 @@ type Props = { }; export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { + const { toast } = useToast(); const [isOpen, setIsOpen] = useState(false); const [selectedDate, setSelectedDate] = useState(new Date()); const [analyticsData, setAnalyticsData] = useState<{ @@ -32,6 +35,11 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { .then(setAnalyticsData) .catch((err) => { console.error("Failed to load analytics", err); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to load analytics", + }); }); }, []); From 3153e9984eb692d65612f9e8e9ae1301d30e7569 Mon Sep 17 00:00:00 2001 From: janayagarcia Date: Wed, 28 May 2025 14:24:32 -0700 Subject: [PATCH 16/20] fixed lint error about catch --- components/StudentAnalyticsDrawer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx index ef95bd0..3bf2920 100644 --- a/components/StudentAnalyticsDrawer.tsx +++ b/components/StudentAnalyticsDrawer.tsx @@ -33,8 +33,12 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { useEffect(() => { getStudentAnalytics(courseId, studentId) .then(setAnalyticsData) - .catch((err) => { - console.error("Failed to load analytics", err); + .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", From 51daeec09bc8623aff054f8e18b9611f53246c7f Mon Sep 17 00:00:00 2001 From: janayagarcia Date: Wed, 28 May 2025 14:26:53 -0700 Subject: [PATCH 17/20] remove apostrophe and put symbol value --- components/StudentAnalyticsDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx index 3bf2920..ec94701 100644 --- a/components/StudentAnalyticsDrawer.tsx +++ b/components/StudentAnalyticsDrawer.tsx @@ -82,7 +82,7 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => {
- Student's Performance + Student's Performance
From 2ecc06a2bc18a119ce6c5612d86fcde14dcc4aff Mon Sep 17 00:00:00 2001 From: janayagarcia Date: Tue, 3 Jun 2025 06:06:56 -0700 Subject: [PATCH 18/20] Added question table component and removed isOpen variable --- components/QuestionResponseTable.tsx | 43 ++++++++++++++++++++++++++ components/StudentAnalyticsDrawer.tsx | 44 +++------------------------ 2 files changed, 47 insertions(+), 40 deletions(-) create mode 100644 components/QuestionResponseTable.tsx diff --git a/components/QuestionResponseTable.tsx b/components/QuestionResponseTable.tsx new file mode 100644 index 0000000..838c5d3 --- /dev/null +++ b/components/QuestionResponseTable.tsx @@ -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 = ({ questions }) => { + return ( +
+ {questions.map((question, idx) => ( +
+
+
{question.text}
+
+ + {question.type === "MCQ" ? "Multiple Choice" : "Multi-Select"} + +
+
+
+ {question.inputtedAnswers + .map((optId) => question.options.find((o) => o.id === optId)?.text) + .join(", ") || "—"} +
+
+ {question.correctAnswers + .map((optId) => question.options.find((o) => o.id === optId)?.text) + .join(", ")} +
+
+ ))} +
+ ); +}; \ No newline at end of file diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx index ec94701..36c467f 100644 --- a/components/StudentAnalyticsDrawer.tsx +++ b/components/StudentAnalyticsDrawer.tsx @@ -9,6 +9,8 @@ import DonutChart from "@/components/ui/DonutChart"; import { Button } from "@/components/ui/button"; import { getQuestionsAndResponsesForDate, getStudentAnalytics } from "@/services/analytics"; import { useToast } from "@/hooks/use-toast"; +import { QuestionResponseTable } from "@/components/QuestionResponseTable"; + type Props = { @@ -18,7 +20,6 @@ type Props = { export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { const { toast } = useToast(); - const [isOpen, setIsOpen] = useState(false); const [selectedDate, setSelectedDate] = useState(new Date()); const [analyticsData, setAnalyticsData] = useState<{ fullName: string; @@ -67,7 +68,7 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { }, [selectedDate]); return ( - + @@ -203,44 +204,7 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => {
-
- {questionsForDate.map((question, idx) => ( -
-
-
- {question.text} -
-
- - {question.type === "MCQ" - ? "Multiple Choice" - : "Multi-Select"} - -
-
-
- {question.inputtedAnswers - .map( - (optId) => - question.options.find( - (o) => o.id === optId, - )?.text, - ) - .join(", ") || "—"} -
-
- {question.correctAnswers - .map( - (optId) => - question.options.find( - (o) => o.id === optId, - )?.text, - ) - .join(", ")} -
-
- ))} -
+
)}
From b039ae34d8618fc3977931a3e6789e802dda72e1 Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 3 Jun 2025 13:34:16 -0700 Subject: [PATCH 19/20] minor refactor --- components/StudentAnalyticsDrawer.tsx | 28 +++++++++++++-------------- components/ui/DatePicker.tsx | 2 +- components/ui/StudentTable.tsx | 9 ++++++--- lib/constants.ts | 10 ++++++++++ lib/utils.ts | 1 + 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx index 36c467f..a766809 100644 --- a/components/StudentAnalyticsDrawer.tsx +++ b/components/StudentAnalyticsDrawer.tsx @@ -10,11 +10,13 @@ import { Button } from "@/components/ui/button"; import { getQuestionsAndResponsesForDate, getStudentAnalytics } from "@/services/analytics"; import { useToast } from "@/hooks/use-toast"; import { QuestionResponseTable } from "@/components/QuestionResponseTable"; - - +import { + studentAnalyticsAttendanceChartConfig, + studentAnalyticsScoreChartConfig, +} from "@/lib/constants"; type Props = { - studentId: string; + studentId: string | null; courseId: number; }; @@ -32,9 +34,10 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { } | null>(null); useEffect(() => { + if (!studentId) return; getStudentAnalytics(courseId, studentId) .then(setAnalyticsData) - .catch ((err: unknown) => { + .catch((err: unknown) => { if (err instanceof Error) { console.error("Failed to load analytics", err); } else { @@ -46,7 +49,7 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { description: "Failed to load analytics", }); }); - }, []); + }, [courseId, studentId]); type QuestionForDate = { id: number; @@ -61,6 +64,7 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { useEffect(() => { const fetchQuestions = async () => { + if (!studentId) return; const data = await getQuestionsAndResponsesForDate(courseId, studentId, selectedDate); setQuestionsForDate(data); }; @@ -70,7 +74,9 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { return ( - + @@ -100,10 +106,7 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { fill: "#FFFFFF", }, ]} - chartConfig={{ - Correct: { label: "Correct", color: "#BFF2A7" }, - Incorrect: { label: "Incorrect", color: "#FFFFFF" }, - }} + chartConfig={studentAnalyticsScoreChartConfig} dataKey="value" nameKey="name" description="Average Poll Score" @@ -140,10 +143,7 @@ export const StudentAnalyticsDrawer = ({ studentId, courseId }: Props) => { fill: "#FFFFFF", }, ]} - chartConfig={{ - Correct: { label: "Attended", color: "#A7F2C2" }, - Incorrect: { label: "Missed", color: "#FFFFFF" }, - }} + chartConfig={studentAnalyticsAttendanceChartConfig} dataKey="value" nameKey="name" description="Attendance" diff --git a/components/ui/DatePicker.tsx b/components/ui/DatePicker.tsx index b346b87..0a22a6b 100644 --- a/components/ui/DatePicker.tsx +++ b/components/ui/DatePicker.tsx @@ -11,7 +11,7 @@ interface Props { export function DatePicker({ currentDate, onSelect }: Props) { return ( - +

{currentDate && format(currentDate, "PPP")}

diff --git a/components/ui/StudentTable.tsx b/components/ui/StudentTable.tsx index 3d8b479..88b6e65 100644 --- a/components/ui/StudentTable.tsx +++ b/components/ui/StudentTable.tsx @@ -13,6 +13,7 @@ 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; @@ -20,6 +21,7 @@ interface Props { export default function StudentTable({ courseId }: Props) { const [students, setStudents] = useState< { + id: string; name: string; email: string | null; attendance: number; @@ -139,9 +141,10 @@ export default function StudentTable({ courseId }: Props) {
- +
diff --git a/lib/constants.ts b/lib/constants.ts index 9daa0eb..7191bda 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -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"]; diff --git a/lib/utils.ts b/lib/utils.ts index 5d3281b..7bdbc64 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -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, From 348af7b5d765c8b6aa7f40b71b18f61a0686df81 Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 3 Jun 2025 13:58:25 -0700 Subject: [PATCH 20/20] lint fix --- components/QuestionResponseTable.tsx | 54 +++++++++++++-------------- components/StudentAnalyticsDrawer.tsx | 5 +-- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/components/QuestionResponseTable.tsx b/components/QuestionResponseTable.tsx index 838c5d3..afb4b30 100644 --- a/components/QuestionResponseTable.tsx +++ b/components/QuestionResponseTable.tsx @@ -10,34 +10,34 @@ type Question = { }; interface Props { - questions: Question[]; + questions: Question[]; } export const QuestionResponseTable: React.FC = ({ questions }) => { - return ( -
- {questions.map((question, idx) => ( -
-
-
{question.text}
-
- - {question.type === "MCQ" ? "Multiple Choice" : "Multi-Select"} - -
-
-
- {question.inputtedAnswers - .map((optId) => question.options.find((o) => o.id === optId)?.text) - .join(", ") || "—"} -
-
- {question.correctAnswers - .map((optId) => question.options.find((o) => o.id === optId)?.text) - .join(", ")} -
+ return ( +
+ {questions.map((question, idx) => ( +
+
+
{question.text}
+
+ + {question.type === "MCQ" ? "Multiple Choice" : "Multi-Select"} + +
+
+
+ {question.inputtedAnswers + .map((optId) => question.options.find((o) => o.id === optId)?.text) + .join(", ") || "—"} +
+
+ {question.correctAnswers + .map((optId) => question.options.find((o) => o.id === optId)?.text) + .join(", ")} +
+
+ ))}
- ))} -
- ); -}; \ No newline at end of file + ); +}; diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx index a766809..c21940d 100644 --- a/components/StudentAnalyticsDrawer.tsx +++ b/components/StudentAnalyticsDrawer.tsx @@ -4,16 +4,15 @@ 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 { Button } from "@/components/ui/button"; -import { getQuestionsAndResponsesForDate, getStudentAnalytics } from "@/services/analytics"; import { useToast } from "@/hooks/use-toast"; -import { QuestionResponseTable } from "@/components/QuestionResponseTable"; import { studentAnalyticsAttendanceChartConfig, studentAnalyticsScoreChartConfig, } from "@/lib/constants"; +import { getQuestionsAndResponsesForDate, getStudentAnalytics } from "@/services/analytics"; type Props = { studentId: string | null;