(null);
@@ -126,36 +115,33 @@ export default function LivePoll({
} finally {
setLoading(false);
}
- }, [courseSessionId, toast, router]); // Added dependencies
+ }, [courseSessionId]); // Added dependencies
// Add this new handler but keep existing code
- const handleWebSocketMessage = useCallback(
- (data: WebSocketMessage) => {
- if (data?.type) {
- if (data.type === "question_changed" && "questionId" in data) {
- activeQuestionIdRef.current = null;
- void fetchActiveQuestion();
- } else if (data.type === "response_saved") {
- toast({ description: data.message ?? "Response saved" });
- setSubmitting(false);
- } else if (data.type === "error") {
- toast({
- variant: "destructive",
- description: data.message ?? "Error occurred",
- });
- setSubmitting(false);
- } else if (data.type === "connected") {
- console.log("WebSocket connection confirmed:", data.message);
- } else if (data.type === "poll_paused" && "paused" in data) {
- setIsPaused(data.paused);
- }
+ const handleWebSocketMessage = useCallback((data: WebSocketMessage) => {
+ if (data?.type) {
+ if (data.type === "question_changed" && "questionId" in data) {
+ activeQuestionIdRef.current = null;
+ void fetchActiveQuestion();
+ } else if (data.type === "response_saved") {
+ toast({ description: data.message ?? "Response saved" });
+ setSubmitting(false);
+ } else if (data.type === "error") {
+ toast({
+ variant: "destructive",
+ description: data.message ?? "Error occurred",
+ });
+ setSubmitting(false);
+ } else if (data.type === "connected") {
+ console.log("WebSocket connection confirmed:", data.message);
+ } else if (data.type === "poll_paused" && "paused" in data) {
+ setIsPaused(data.paused);
}
- },
- [fetchActiveQuestion, toast],
- );
+ }
+ }, []);
// Add this alongside existing WebSocket setup
- const _newWsRef = usePollSocket({
+ const wsRef = usePollSocket({
courseSessionId,
userId: session?.user?.id ?? "",
onMessage: handleWebSocketMessage,
@@ -163,139 +149,8 @@ export default function LivePoll({
// Keep existing WebSocket setup
useEffect(() => {
- if (!courseSessionId || !session?.user?.id) return;
-
- let reconnectAttempts = 0;
- const maxReconnectAttempts = 5;
- const reconnectDelay = 1000; // Start with 1 second
-
- const connectWebSocket = () => {
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
- const ws = new WebSocket(
- `${protocol}//${window.location.host}/ws/poll?sessionId=${courseSessionId}&userId=${session.user.id}`,
- );
- wsRef.current = ws;
-
- ws.onopen = () => {
- console.log("WebSocket connection established");
- setIsConnected(true);
- setMessages((prev) => [...prev, "Connected to WebSocket"]);
- reconnectAttempts = 0; // Reset reconnect attempts on successful connection
- };
-
- ws.onmessage = (event) => {
- let data: WebSocketMessage | null = null;
- let messageText = "";
-
- // Display the raw message for debugging
- console.log("Raw message received:", event.data);
-
- // Try to parse as JSON, but handle plain text too
- try {
- if (typeof event.data === "string") {
- try {
- // Try to parse as JSON
- data = JSON.parse(event.data) as WebSocketMessage;
- messageText = `Received JSON: ${JSON.stringify(data)}`;
- console.log("Parsed JSON:", data);
-
- // Process valid JSON message
- if (data?.type) {
- if (data.type === "question_changed" && "questionId" in data) {
- activeQuestionIdRef.current = null;
- void fetchActiveQuestion();
- } else if (data.type === "response_saved") {
- toast({ description: data.message ?? "Response saved" });
- setSubmitting(false);
- } else if (data.type === "error") {
- toast({
- variant: "destructive",
- description: data.message ?? "Error occurred",
- });
- setSubmitting(false);
- } else if (data.type === "connected") {
- console.log("WebSocket connection confirmed:", data.message);
- } else if (data.type === "echo") {
- console.log("Server echo:", data.message);
- } else if (data.type === "poll_paused") {
- if ("paused" in data) {
- setIsPaused(data.paused);
- }
- }
- }
- } catch {
- const message = event.data;
- messageText = `Received text: ${message}`;
-
- // Check if this is a response to our student submission
- if (
- typeof message === "string" &&
- message.includes("student_response") &&
- submitting
- ) {
- // likely a response to our student submission
- toast({ description: "Your answer has been recorded" });
- setSubmitting(false);
- }
- }
- } else {
- data = {
- type: "binary",
- message: "Binary data received",
- };
- messageText = "Received: Binary data";
- console.log("Received binary data");
- }
-
- setMessages((prev) => [...prev, messageText]);
- } catch (err: unknown) {
- const errorStr = err instanceof Error ? err.message : "Unknown error";
- console.error("Error processing message:", errorStr);
- setMessages((prev) => [...prev, `Error processing message: ${errorStr}`]);
- setSubmitting(false);
- }
- };
-
- ws.onclose = () => {
- console.log("WebSocket connection closed");
- setIsConnected(false);
- setMessages((prev) => [...prev, "Disconnected from WebSocket"]);
- setSubmitting(false);
-
- // Attempt to reconnect if we haven't exceeded max attempts
- if (reconnectAttempts < maxReconnectAttempts) {
- reconnectAttempts++;
- const delay = reconnectDelay * Math.pow(2, reconnectAttempts - 1); // Exponential backoff
- console.log(
- `Attempting to reconnect in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`,
- );
- setTimeout(connectWebSocket, delay);
- } else {
- toast({
- variant: "destructive",
- description: "Lost connection to server. Please refresh the page.",
- });
- }
- };
-
- ws.onerror = (wsError) => {
- console.error("WebSocket error:", wsError);
- setMessages((prev) => [...prev, "WebSocket error occurred"]);
- setSubmitting(false);
- };
-
- // Initial fetch
- void fetchActiveQuestion();
- };
-
- connectWebSocket();
-
- return () => {
- if (wsRef.current?.readyState === WebSocket.OPEN) {
- wsRef.current.close();
- }
- };
- }, [courseSessionId, session?.user?.id, fetchActiveQuestion, toast]);
+ void fetchActiveQuestion();
+ }, []);
// Handle loading state
if ((loading && !currentQuestion) || isAccessLoading) {
@@ -397,9 +252,9 @@ export default function LivePoll({
-
{isConnected ? "Connected" : "Disconnected"}
+
{wsRef.current?.OPEN ? "Connected" : "Disconnected"}
@@ -434,7 +289,7 @@ export default function LivePoll({
!selectedValues ||
(Array.isArray(selectedValues) && selectedValues.length === 0) ||
submitting ||
- !isConnected
+ !wsRef.current?.OPEN
}
className={`mt-6 px-6 py-2 rounded-lg text-white font-medium ${
submitting ||
diff --git a/components/StudentAnalyticsDrawer.tsx b/components/StudentAnalyticsDrawer.tsx
index 30ebcf4..f2a529e 100644
--- a/components/StudentAnalyticsDrawer.tsx
+++ b/components/StudentAnalyticsDrawer.tsx
@@ -1,6 +1,5 @@
"use client";
-import { formatDateToISO } from "@/lib/utils";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { format } from "date-fns";
import { useEffect, useState } from "react";
@@ -12,7 +11,8 @@ import { useToast } from "@/hooks/use-toast";
import {
studentAnalyticsAttendanceChartConfig,
studentAnalyticsScoreChartConfig,
-} from "@/lib/constants";
+} from "@/lib/charts";
+import { formatDateToISO } from "@/lib/utils";
import { getQuestionsAndResponsesForDate, getStudentAnalytics } from "@/services/analytics";
type Props = {
diff --git a/components/YAxisTick.tsx b/components/YAxisTick.tsx
index 1935014..e31f63b 100644
--- a/components/YAxisTick.tsx
+++ b/components/YAxisTick.tsx
@@ -1,6 +1,4 @@
-import { MoreHorizontal } from "lucide-react";
-import React from "react";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { StringTooltipContainer, Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
interface EllipsisYAxisTickProps {
x?: number;
@@ -11,39 +9,20 @@ interface EllipsisYAxisTickProps {
export function LetteredYAxisTick({ x = 0, y = 0, payload }: EllipsisYAxisTickProps) {
if (!payload) return null;
- const fullText = payload.value;
- const maxChars = 15;
- const truncated =
- fullText.length > maxChars ? fullText.slice(0, maxChars) + " . . . " : fullText;
-
return (
-
- {truncated}
-
- {fullText.length > maxChars && (
-
-
-
-
-
-
-
- {fullText}
-
-
-
-
- )}
+
+
+
+
+ {payload.value}
+
+
+
+
+
+
+
);
}
diff --git a/components/ui/AttendanceLineChart.tsx b/components/ui/AttendanceLineChart.tsx
index 9e1d02b..ea2d78c 100644
--- a/components/ui/AttendanceLineChart.tsx
+++ b/components/ui/AttendanceLineChart.tsx
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from "react";
import { ChartContainer } from "@/components/ui/chart";
import { CartesianGrid, Line, LineChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
import dayjs from "dayjs";
-import { attendanceChartConfig } from "@/lib/constants";
+import { attendanceChartConfig } from "@/lib/charts";
import { calculateWeekAttendance } from "@/lib/utils";
import { useToast } from "@/hooks/use-toast";
import LoaderComponent from "./loader";
diff --git a/components/ui/DonutChart.tsx b/components/ui/DonutChart.tsx
index c20ed0d..7cfccdd 100644
--- a/components/ui/DonutChart.tsx
+++ b/components/ui/DonutChart.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import React from "react";
import {
ChartConfig,
diff --git a/components/ui/PastQuestions.tsx b/components/ui/PastQuestions.tsx
index 0d1dffd..776148a 100644
--- a/components/ui/PastQuestions.tsx
+++ b/components/ui/PastQuestions.tsx
@@ -8,21 +8,7 @@ import {
} from "@/components/ui/select";
import { Question, QuestionType } from "@prisma/client";
import Link from "next/link";
-
-const questionTypeStyles = {
- [QuestionType.MCQ]: {
- bgColor: "#FFFED3",
- textColor: "#58560B",
- borderColor: "#58570B",
- label: "Multiple Choice",
- },
- [QuestionType.MSQ]: {
- bgColor: "#EBCFFF",
- textColor: "#602E84",
- borderColor: "#602E84",
- label: "Select-All",
- },
-};
+import { questionTypeColors, questionTypeMap } from "@/lib/constants";
interface PastQuestion extends Question {
session: { startTime: Date };
@@ -158,14 +144,12 @@ function PastQuestions({ courseId }: Props) {
- {questionTypeStyles[question.type].label}
+ {questionTypeMap[question.type]}
{question.text}
diff --git a/components/ui/PerformanceData.tsx b/components/ui/PerformanceData.tsx
index 504aafb..a66a591 100644
--- a/components/ui/PerformanceData.tsx
+++ b/components/ui/PerformanceData.tsx
@@ -1,13 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import DonutChart from "@/components/ui/DonutChart";
-import {
- dataKey,
- description,
- nameKey,
- performanceChartConfig,
- questionTypeColors,
- questionTypeMap,
-} from "@/lib/constants";
+import { performanceChartConfig } from "@/lib/charts";
+import { questionTypeColors, questionTypeMap } from "@/lib/constants";
import { useToast } from "@/hooks/use-toast";
import { getLimitedPastQuestions, getResponses } from "@/services/question";
import { getIncorrectAndCorrectResponseCounts, getQuestionsWithAverageScore } from "@/lib/utils";
@@ -88,9 +82,9 @@ export default function PerformanceData({ courseId }: Props) {
-
-
-
-
-
- Class Average:
- {Math.round(value)}%
-
-
- );
-}
diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
index d07e1ce..34ecf9c 100644
--- a/components/ui/tooltip.tsx
+++ b/components/ui/tooltip.tsx
@@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ "z-50 overflow-hidden animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
@@ -31,7 +31,7 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
const StringTooltipContainer = ({ text }: { text: string }) => {
return (
-
+
{text}
);
diff --git a/hooks/use-poll-socket.ts b/hooks/use-poll-socket.ts
index e0b15bf..39617e3 100644
--- a/hooks/use-poll-socket.ts
+++ b/hooks/use-poll-socket.ts
@@ -61,7 +61,7 @@ export function usePollSocket({
wsRef.current.close();
}
};
- }, [courseSessionId, userId, onConnect, onDisconnect, onMessage]);
+ }, [courseSessionId, userId]);
return wsRef;
}
diff --git a/lib/charts.ts b/lib/charts.ts
new file mode 100644
index 0000000..090563e
--- /dev/null
+++ b/lib/charts.ts
@@ -0,0 +1,32 @@
+import { ChartConfig } from "@/components/ui/chart";
+
+export const performanceChartConfig = {
+ count: {
+ label: "Count",
+ },
+ correct: {
+ label: "Correct",
+ color: "green",
+ },
+ incorrect: {
+ label: "Incorrect",
+ color: "gray",
+ },
+} satisfies ChartConfig;
+
+export const attendanceChartConfig = {
+ attendance: {
+ label: "Attendance",
+ color: "black",
+ },
+} 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;
diff --git a/lib/constants.ts b/lib/constants.ts
index 0d9870f..b3b08bf 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -1,9 +1,6 @@
-import { $Enums } from "@prisma/client";
-import { ChartConfig } from "@/components/ui/chart";
-
export const questionTypes = ["Multiple Choice", "Select All"] as const;
-export const colorOptions = ["#ED9D9D", "#F3AB7E", "#EEF583", "#94ED79", "#8E87F2"];
+export const colorOptions = ["#ED9D9D", "#F3AB7E", "#EEF583", "#94ED79", "#8E87F2"];
export const daysOptions = ["M", "T", "W", "Th", "F"] as const;
export const dayLabels: Record<(typeof daysOptions)[number], string> = {
@@ -24,97 +21,11 @@ export const questionTypeColors = {
MCQ: { bg: "#EBCFFF", fg: "#602E84" },
};
-// donut chart config
-export const dataKey = "count";
-export const nameKey = "result";
-export const description = "Class Average";
-export const performanceChartConfig = {
- count: {
- label: "Count",
- },
- correct: {
- label: "Correct",
- color: "green",
- },
- incorrect: {
- label: "Incorrect",
- color: "gray",
- },
-} satisfies ChartConfig;
-
-export const attendanceChartConfig = {
- attendance: {
- label: "Attendance",
- color: "black",
- },
-} 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"];
+export const coursePages = ["Questionnaire", "Analytics", "Admin"] as const;
export const DEFAULT_SHOW_RESULTS = false;
-export type QuestionWithResponesAndOptions = {
- options: {
- isCorrect: boolean;
- text: string;
- id: number;
- questionId: number;
- }[];
- responses: {
- userId: string;
- questionId: number;
- optionId: number;
- answeredAt: Date;
- }[];
-} & {
- text: string;
- id: number;
- type: $Enums.QuestionType;
- sessionId: number;
- position: number;
-};
-
-export type Response = {
- options: {
- isCorrect: boolean;
- id: number;
- text: string;
- questionId: number;
- }[];
- responses: {
- optionId: number;
- questionId: number;
- userId: string;
- answeredAt: Date;
- }[];
-};
-
-export type Student = {
- id: string;
- responses: {
- question: {
- sessionId: number;
- options: {
- isCorrect: boolean;
- }[];
- };
- }[];
- email: string | null;
- firstName: string;
- lastName: string | null;
-};
-
export const csvBasicFieldNames = ["email", "num_questions_answered", "date_of_session"];
export const csvAdvancedFieldNames = [
"email",
diff --git a/lib/utils.ts b/lib/utils.ts
index 7bdbc64..0e664b8 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -2,7 +2,11 @@ import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import dayjs from "dayjs";
import { twMerge } from "tailwind-merge";
-import { QuestionWithResponesAndOptions, Response, Student } from "./constants";
+import {
+ QuestionWithResponesAndOptions,
+ ResponseWithOptions,
+ StudentWithResponses,
+} from "@/models/Analytics";
import { getAttendanceCount, getStudentCount } from "@/services/userCourse";
/**
* A utility function that merges tailwind classes with conditional classes combining functionalities of twMerge and clsx.
@@ -27,8 +31,8 @@ export function greetUser(name: string): string {
/**
* A utility function that replaces date objects time to 00:00:00 and returns ISO String.
*
- * @param date - The name to greet.
- * @returns A greeting message.
+ * @param date - Date object to convert.
+ * @returns Formattted Date object.
*/
export function formatDateToISO(date: Date) {
return new Date(date.setHours(0, 0, 0, 0)).toISOString();
@@ -74,7 +78,7 @@ export function getQuestionsWithAverageScore(questions: QuestionWithResponesAndO
return questionsWithScores;
}
-export function getIncorrectAndCorrectResponseCounts(responses: Response[]) {
+export function getIncorrectAndCorrectResponseCounts(responses: ResponseWithOptions[]) {
let correctResponses = 0;
let incorrectReponses = 0;
@@ -97,7 +101,7 @@ export function getIncorrectAndCorrectResponseCounts(responses: Response[]) {
return { incorrect: incorrectReponses, correct: correctResponses };
}
-export function getStudentsWithScores(students: Student[], sessionIds: number[]) {
+export function getStudentsWithScores(students: StudentWithResponses[], sessionIds: number[]) {
const studentData = students.map((student) => {
// get total number of sessions
const totalSessions = sessionIds.length;
diff --git a/models/Analytics.ts b/models/Analytics.ts
new file mode 100644
index 0000000..abc5483
--- /dev/null
+++ b/models/Analytics.ts
@@ -0,0 +1,23 @@
+import { Option, Question, Response as ResponseSchema } from "@prisma/client";
+
+export type QuestionWithResponesAndOptions = ResponseWithOptions & Question;
+
+export type ResponseWithOptions = {
+ options: Option[];
+ responses: ResponseSchema[];
+};
+
+export type StudentWithResponses = {
+ id: string;
+ responses: {
+ question: {
+ sessionId: number;
+ options: {
+ isCorrect: boolean;
+ }[];
+ };
+ }[];
+ email: string | null;
+ firstName: string;
+ lastName: string | null;
+};
diff --git a/types/ExportCSVType.ts b/models/ExportCSVType.ts
similarity index 100%
rename from types/ExportCSVType.ts
rename to models/ExportCSVType.ts
diff --git a/models/User.ts b/models/User.ts
deleted file mode 100644
index 04d7091..0000000
--- a/models/User.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// Example
-
-export type User = {
- id: number;
- name: string;
- email: string;
- createdAt: string;
- updatedAt: string;
-};
diff --git a/services/analytics.ts b/services/analytics.ts
index 6e89f5a..98a12b8 100644
--- a/services/analytics.ts
+++ b/services/analytics.ts
@@ -94,11 +94,10 @@ export async function getQuestionsAndResponsesForDate(
courseId: number,
studentId: string,
isoDate: string,
-
) {
- const start = new Date(isoDate)
- const end = new Date(start)
- end.setUTCHours(23, 59, 59, 999)
+ const start = new Date(isoDate);
+ const end = new Date(start);
+ end.setUTCHours(23, 59, 59, 999);
try {
const sessions = await prisma.courseSession.findMany({
where: {
diff --git a/services/userCourse.ts b/services/userCourse.ts
index 1df2936..ccc784f 100644
--- a/services/userCourse.ts
+++ b/services/userCourse.ts
@@ -39,16 +39,17 @@ export async function getUserCourses(userId: string) {
},
});
- // const courses = await Promise.all(
- // userCourses.map(async (course) => await getCourseWithId(course.courseId)),
- // );
return courses.map(({ course, role }) => ({
...course,
role,
}));
}
-export async function validateUser(userId: string, courseId: number, role: Role): Promise
{
+export async function validateUser(
+ userId: string,
+ courseId: number,
+ role?: Role,
+): Promise {
const userCourse = await prisma.userCourse.findFirst({
where: {
userId,