{Array.from({ length: 3 }).map((_, index) => {
@@ -52,6 +42,14 @@ export const StudentsLaboratoryView = () => {
);
}
+ // Handle error state
+ if (isLaboratoryError) {
+ toast.error(laboratoryError.message);
+ navigate(`/courses/${courseUUID}/laboratories`);
+ return;
+ }
+
+ // TODO: Display an error component if its not loading but there is no laboratory
if (!laboratory) return null;
return (
diff --git a/src/screens/complete-laboratory/components/StudentLaboratoryBlocks.tsx b/src/screens/complete-laboratory/components/StudentLaboratoryBlocks.tsx
index 18d3e19f..80feab58 100644
--- a/src/screens/complete-laboratory/components/StudentLaboratoryBlocks.tsx
+++ b/src/screens/complete-laboratory/components/StudentLaboratoryBlocks.tsx
@@ -1,19 +1,11 @@
-import { LaboratoryBlockSkeleton } from "@/components/Skeletons/LaboratoryBlockSkeleton";
import {
LaboratoryBlock,
MarkdownBlock,
TestBlock
} from "@/types/entities/laboratory-entities";
-import { Suspense } from "react";
-import { lazily } from "react-lazily";
-const { MarkdownPreviewBlock } = lazily(
- () => import("./markdown-block/MarkdownPreviewBlock")
-);
-
-const { TestPreviewBlock } = lazily(
- () => import("./test-block/TestPreviewBlock")
-);
+import { MarkdownPreviewBlock } from "./markdown-block/MarkdownPreviewBlock";
+import { TestPreviewBlock } from "./test-block/TestPreviewBlock";
interface StudentLaboratoryBlocksProps {
blocks: LaboratoryBlock[];
@@ -28,16 +20,19 @@ export const StudentLaboratoryBlocks = ({
if (block.blockType === "markdown") {
const mdBlock: MarkdownBlock = block as MarkdownBlock;
return (
-
}>
-
-
+
);
} else {
const testBlock: TestBlock = block as TestBlock;
return (
-
}>
-
-
+
);
}
})}
diff --git a/src/screens/complete-laboratory/components/test-block/.gitkeep b/src/screens/complete-laboratory/components/test-block/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/screens/complete-laboratory/components/test-block/TestPreviewBlock.tsx b/src/screens/complete-laboratory/components/test-block/TestPreviewBlock.tsx
index 6ac767ca..ba59f285 100644
--- a/src/screens/complete-laboratory/components/test-block/TestPreviewBlock.tsx
+++ b/src/screens/complete-laboratory/components/test-block/TestPreviewBlock.tsx
@@ -1,6 +1,9 @@
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { TestBlock } from "@/types/entities/laboratory-entities";
+import { useState } from "react";
import { TestPreviewBlockForm } from "./TestPreviewBlockForm";
+import { TestStatus } from "./TestStatus";
interface TestPreviewBlockProps {
block: TestBlock;
@@ -11,9 +14,41 @@ export const TestPreviewBlock = ({
block,
blockIndex
}: TestPreviewBlockProps) => {
+ const defaultActiveTab = `${block.uuid}-form`;
+ const [activeTab, setActiveTab] = useState(defaultActiveTab);
+
return (
-
-
-
+
+
+ setActiveTab(`${block.uuid}-form`)}
+ aria-label={`Test block ${blockIndex + 1} submission form`}
+ >
+ Submission Form
+
+ setActiveTab(`${block.uuid}-status`)}
+ aria-label={`Test block ${blockIndex + 1} submission status`}
+ >
+ Submission Status
+
+
+
+ setActiveTab(`${block.uuid}-status`)}
+ />
+
+
+ setActiveTab(`${block.uuid}-form`)}
+ />
+
+
);
};
diff --git a/src/screens/complete-laboratory/components/test-block/TestPreviewBlockForm.tsx b/src/screens/complete-laboratory/components/test-block/TestPreviewBlockForm.tsx
index aab6a171..5741bae4 100644
--- a/src/screens/complete-laboratory/components/test-block/TestPreviewBlockForm.tsx
+++ b/src/screens/complete-laboratory/components/test-block/TestPreviewBlockForm.tsx
@@ -10,11 +10,12 @@ import {
import { Input } from "@/components/ui/input";
import { CONSTANTS } from "@/config/constants";
import { getSupportedLanguagesService } from "@/services/languages/get-supported-languages.service";
+import { submitToTestBlockService } from "@/services/submissions/submit-to-test-block.service";
import { useSupportedLanguagesStore } from "@/stores/supported-languages-store";
import { TestBlock } from "@/types/entities/laboratory-entities";
import { downloadLanguageTemplate } from "@/utils/utils";
import { zodResolver } from "@hookform/resolvers/zod";
-import { DownloadIcon } from "lucide-react";
+import { DownloadIcon, SendIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -41,11 +42,13 @@ const testPreviewBlockFormScheme = z.object({
interface TestPreviewBlockFormProps {
testBlock: TestBlock;
blockIndex: number;
+ changeToStatusTabCallback: () => void;
}
export const TestPreviewBlockForm = ({
testBlock,
- blockIndex
+ blockIndex,
+ changeToStatusTabCallback
}: TestPreviewBlockFormProps) => {
const [isSending, setIsSending] = useState(false);
@@ -90,10 +93,27 @@ export const TestPreviewBlockForm = ({
data: z.infer
) => {
setIsSending(true);
- console.log(data);
+ await submitArchive(data);
setIsSending(false);
};
+ const submitArchive = async (
+ data: z.infer
+ ) => {
+ const { success, message } = await submitToTestBlockService({
+ testBlockUUID: testBlock.uuid,
+ submissionArchive: data.submissionFile
+ });
+
+ if (!success) {
+ toast.error(message);
+ return;
+ }
+
+ toast.success(message);
+ changeToStatusTabCallback();
+ };
+
return (
+
{testBlock.submissionUUID && (
-
);
diff --git a/src/screens/complete-laboratory/components/test-block/TestStatus.tsx b/src/screens/complete-laboratory/components/test-block/TestStatus.tsx
new file mode 100644
index 00000000..f47e28c0
--- /dev/null
+++ b/src/screens/complete-laboratory/components/test-block/TestStatus.tsx
@@ -0,0 +1,111 @@
+import { getSubmissionRealTimeStatusService } from "@/services/submissions/get-submission-real-time-status.service";
+import { TestBlock } from "@/types/entities/laboratory-entities";
+import {
+ submissionStatus,
+ submissionUpdate
+} from "@/types/entities/submission-entities";
+import { parseSubmissionSSEUpdate } from "@/utils/utils";
+import { useEffect, useRef, useState } from "react";
+import { toast } from "sonner";
+
+import { TestStatusPhase } from "./TestStatusPhase";
+
+interface TestStatusProps {
+ blockIndex: number;
+ testBlock: TestBlock;
+ changeToFormTabCallback: () => void;
+}
+
+const status: submissionStatus[] = ["pending", "running", "ready"];
+
+const initialStatusUpdate: submissionUpdate = {
+ submissionStatus: "pending",
+ testsPassed: false,
+ submissionUUID: "",
+ testsOutput: ""
+};
+
+export const TestStatus = ({
+ testBlock,
+ changeToFormTabCallback
+}: TestStatusProps) => {
+ // Submission status state
+ const [currentStatusUpdate, setCurrentStatusUpdate] =
+ useState(initialStatusUpdate);
+
+ const currentStatusIndex = status.indexOf(
+ currentStatusUpdate.submissionStatus
+ );
+
+ // Event source ref
+ const eventSourceRef = useRef(null);
+
+ // Get the test status on component mount
+ useEffect(() => {
+ const getRealTimeTestStatus = async () => {
+ const { success, eventSource } = await getSubmissionRealTimeStatusService(
+ testBlock.uuid
+ );
+
+ if (!success || !eventSource) {
+ handleEventSourceError();
+ return;
+ }
+
+ // Listen for errors
+ eventSource.onerror = () => {
+ handleEventSourceError(
+ "We had an error, please, make sure you have sent a submission"
+ );
+ };
+
+ // Listen for incoming updates
+ const CUSTOM_EVENT_NAME = "update";
+ eventSource.addEventListener(CUSTOM_EVENT_NAME, (event) => {
+ const update = parseSubmissionSSEUpdate(event.data);
+
+ // If the submission is ready, set the test output and close the event source
+ if (update.submissionStatus === "ready") {
+ eventSourceRef.current?.OPEN && eventSourceRef.current?.close();
+ }
+
+ // Update the state
+ setCurrentStatusUpdate(update);
+ });
+
+ // Set the event source
+ eventSourceRef.current = eventSource;
+ };
+
+ getRealTimeTestStatus();
+
+ // Unsubscribe from the event source on component unmount
+ return () => {
+ eventSourceRef.current?.OPEN && eventSourceRef.current.close();
+ };
+ }, []);
+
+ const handleEventSourceError = (
+ errorMessage: string = "We had an error while obtaining the current status of your submission"
+ ) => {
+ toast.error(errorMessage);
+ changeToFormTabCallback();
+ eventSourceRef.current?.OPEN && eventSourceRef.current?.close();
+ };
+
+ return (
+
+ {status.map((phase, index) => {
+ return (
+
+ );
+ })}
+
+ );
+};
diff --git a/src/screens/complete-laboratory/components/test-block/TestStatusPhase.tsx b/src/screens/complete-laboratory/components/test-block/TestStatusPhase.tsx
new file mode 100644
index 00000000..7f406aa8
--- /dev/null
+++ b/src/screens/complete-laboratory/components/test-block/TestStatusPhase.tsx
@@ -0,0 +1,101 @@
+import {
+ submissionStatus,
+ submissionUpdate
+} from "@/types/entities/submission-entities";
+import { CheckIcon, HourglassIcon, Loader2Icon, XIcon } from "lucide-react";
+import { ReactElement } from "react";
+
+interface TestStatusPhaseProps {
+ phaseName: submissionStatus;
+ phaseIndex: number;
+ currentPhase: submissionUpdate;
+ currentPhaseIndex: number;
+}
+
+const DEFAULT_SUCCESS_COLOR_CLASS = "bg-green-500";
+const DEFAULT_ERROR_COLOR_CLASS = "bg-red-500";
+const DEFAULT_PENDING_COLOR_CLASS = "bg-gray-400";
+
+export const TestStatusPhase = ({
+ phaseName,
+ phaseIndex,
+ currentPhase,
+ currentPhaseIndex
+}: TestStatusPhaseProps) => {
+ const wasCurrentStatusPassed = phaseIndex < currentPhaseIndex;
+ const wasCurrentStatusReached = phaseIndex <= currentPhaseIndex;
+
+ const isRunningNow = currentPhase.submissionStatus === "running";
+ const wasRan = currentPhase.submissionStatus === "ready";
+ const mayShowTestOutput = wasRan && phaseName === "ready";
+
+ const getPhaseIconColorClasses = (): string => {
+ if (wasCurrentStatusPassed) {
+ return DEFAULT_SUCCESS_COLOR_CLASS;
+ } else {
+ if (phaseName === "ready" && wasRan) {
+ // If the final status was reached, check if the tests passed
+ const hasFinishedSuccessfully = currentPhase.testsPassed;
+
+ return hasFinishedSuccessfully
+ ? DEFAULT_SUCCESS_COLOR_CLASS
+ : DEFAULT_ERROR_COLOR_CLASS;
+ }
+
+ // If the final status was not reached, return the default color
+ return DEFAULT_PENDING_COLOR_CLASS;
+ }
+ };
+
+ const getPhaseIcon = (): ReactElement => {
+ const icons = {
+ pending: ,
+ running: ,
+ ["ready-success"]: ,
+ ["ready-error"]:
+ };
+
+ let iconKey: keyof typeof icons;
+
+ if (phaseName === "ready" && wasRan) {
+ const hasFinishedSuccessfully = currentPhase.testsPassed;
+ iconKey = hasFinishedSuccessfully ? "ready-success" : "ready-error";
+ } else if (phaseName === "running" && isRunningNow) {
+ iconKey = "running";
+ } else if (wasCurrentStatusPassed) {
+ iconKey = "ready-success";
+ } else {
+ iconKey = "pending";
+ }
+
+ return icons[iconKey];
+ };
+
+ return (
+
+ {/* Status header */}
+
+
+ {getPhaseIcon()}
+
+
+ {phaseName}
+
+
+ {/* Status content */}
+ {mayShowTestOutput && (
+
+
+ {currentPhase.testsOutput}
+
+
+ )}
+
+ );
+};
diff --git a/src/screens/course-page/CoursePageLayout.tsx b/src/screens/course-page/CoursePageLayout.tsx
index 514ebb31..4bebf72b 100644
--- a/src/screens/course-page/CoursePageLayout.tsx
+++ b/src/screens/course-page/CoursePageLayout.tsx
@@ -1,8 +1,8 @@
import { AuthContext } from "@/context/AuthContext";
import { getCourseService } from "@/services/courses/get-course.service";
-import { Course } from "@/types/entities/general-entities";
import { getCourseInitials } from "@/utils/utils";
-import { Fragment, useContext, useEffect, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Fragment, useContext } from "react";
import { Link, Outlet, useNavigate, useParams } from "react-router-dom";
import { toast } from "sonner";
@@ -10,34 +10,33 @@ import { CourseAsideOptions } from "./CourseAsideOptions";
import { CourseNavigationSkeleton } from "./CourseNavigationSkeleton";
export const CoursePageLayout = () => {
- const navigate = useNavigate();
+ // Url state
const { courseUUID = "" } = useParams<{ courseUUID: string }>();
+ const navigate = useNavigate();
+ // Global user state
const { user } = useContext(AuthContext);
const role = user?.role || "student";
+ // Fetching state
+ const {
+ data: course,
+ isLoading,
+ isError: isCourseError,
+ error: courseError
+ } = useQuery({
+ queryKey: ["course", courseUUID],
+ queryFn: () => getCourseService(courseUUID)
+ });
- const [state, setState] = useState<"loading" | "idle">("loading");
- const [course, setCourse] = useState(null);
- const isLoading = state === "loading" || course === null;
-
- useEffect(() => {
- getCourse();
- }, []);
-
- const getCourse = async () => {
- const { success, ...response } = await getCourseService(courseUUID);
- if (!success) {
- toast.error(response.message);
- redirectToCoursesView();
- }
-
- setCourse(response.course);
- setState("idle");
- };
-
- const redirectToCoursesView = () => {
+ if (isCourseError) {
+ toast.error(courseError.message);
navigate("/courses");
- };
+ }
+
+ if (!isLoading && !course) {
+ // TODO: Return the error component if its not loading and there is no course
+ return null;
+ }
return (
@@ -50,12 +49,12 @@ export const CoursePageLayout = () => {
- {getCourseInitials(course.name)}
+ {getCourseInitials(course!.name)}
- {course.name}
+ {course!.name}
{/* Course navigation */}
diff --git a/src/screens/course-page/laboratories/CourseLaboratories.tsx b/src/screens/course-page/laboratories/CourseLaboratories.tsx
index c7da63c7..638f55fd 100644
--- a/src/screens/course-page/laboratories/CourseLaboratories.tsx
+++ b/src/screens/course-page/laboratories/CourseLaboratories.tsx
@@ -1,19 +1,19 @@
-import { CourseLaboratoriesContext } from "@/context/laboratories/CourseLaboratoriesContext";
-import { useSession } from "@/hooks/useSession";
+import { AuthContext } from "@/context/AuthContext";
+import { useCourseLaboratories } from "@/hooks/laboratories/useCourseLaboratories";
import { useContext } from "react";
import { CourseLaboratoriesTable } from "./components/CourseLaboratoriesTable";
import { CreateLaboratoryDialog } from "./dialogs/create-laboratory/CreateLaboratoryDialog";
export const CourseLaboratories = () => {
- const { user } = useSession();
- const { loading, laboratories } = useContext(CourseLaboratoriesContext);
+ const { user } = useContext(AuthContext);
+ const { loading, laboratories } = useCourseLaboratories();
return (
Course laboratories
- {user?.role == "teacher" && }
+ {user!.role == "teacher" && }
{
- const { user } = useSession();
+ const { user } = useContext(AuthContext);
const { courseUUID } = useParams<{ courseUUID: string }>();
+ const getLaboratoryActionsByRole = ({
+ role,
+ labInfo
+ }: {
+ role: string;
+ labInfo: LaboratoryBaseInfo;
+ }) => {
+ if (role === "teacher") {
+ return (
+ <>
+
+ Edit
+
+
+ View progress
+
+ >
+ );
+ } else {
+ return (
+ <>
+
+ Complete
+
+ >
+ );
+ }
+ };
+
if (loading) {
return (
- {laboratories.length ? (
+ {laboratories?.length ? (
laboratories.map((lab) => (
{lab.name}
@@ -62,27 +104,12 @@ export const CourseLaboratoriesTable = ({
{dayjs(lab.due_date).fromNow()}
- {user?.role === "teacher" ? (
- <>
-
- Edit
-
- >
- ) : (
- <>
-
- Complete
-
- >
- )}
+
+ {getLaboratoryActionsByRole({
+ role: user!.role,
+ labInfo: lab
+ })}
+
))
diff --git a/src/screens/course-page/laboratories/dialogs/create-laboratory/CreateLaboratoryForm.tsx b/src/screens/course-page/laboratories/dialogs/create-laboratory/CreateLaboratoryForm.tsx
index 72500dcf..ec202f8c 100644
--- a/src/screens/course-page/laboratories/dialogs/create-laboratory/CreateLaboratoryForm.tsx
+++ b/src/screens/course-page/laboratories/dialogs/create-laboratory/CreateLaboratoryForm.tsx
@@ -9,11 +9,11 @@ import {
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
-import { CourseLaboratoriesContext } from "@/context/laboratories/CourseLaboratoriesContext";
-import { courseLaboratoriesActionType } from "@/hooks/laboratories/courseLaboratoriesReducer";
import { createLaboratoryService } from "@/services/laboratories/create-laboratory.service";
+import { LaboratoryBaseInfo } from "@/types/entities/laboratory-entities";
import { zodResolver } from "@hookform/resolvers/zod";
-import { useContext, useState } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
import { useForm } from "react-hook-form";
import { useParams } from "react-router-dom";
import { toast } from "sonner";
@@ -57,7 +57,10 @@ interface CreateLaboratoryFormProps {
export const CreateLaboratoryForm = ({
closeDialogCallback
}: CreateLaboratoryFormProps) => {
- const [loading, setLoading] = useState(false);
+ const { courseUUID = "" } = useParams<{ courseUUID: string }>();
+
+ // Form state
+ const [isCreatingLab, setIsCreatingLab] = useState(false);
const form = useForm>({
resolver: zodResolver(createLaboratorySchema),
@@ -68,37 +71,52 @@ export const CreateLaboratoryForm = ({
}
});
- const { courseUUID = "" } = useParams<{ courseUUID: string }>();
+ // Create laboratory mutation
+ const queryClient = useQueryClient();
+ const { mutate: createLaboratoryMutation } = useMutation({
+ mutationFn: createLaboratoryService,
+ onMutate: (args) => {
+ setIsCreatingLab(true);
- const { laboratoriesDispatcher } = useContext(CourseLaboratoriesContext);
+ // Forward parameters to the following callbacks
+ return args;
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (newLabUUID, args) => {
+ const { name, openingDate, dueDate } = args;
- const onSubmit = async (values: z.infer) => {
- setLoading(true);
+ const newLaboratory: LaboratoryBaseInfo = {
+ uuid: newLabUUID,
+ name,
+ opening_date: openingDate,
+ due_date: dueDate
+ };
- // Send the request
- const { success, message, laboratoryUUID } = await createLaboratoryService({
- ...values,
- courseUUID
- });
- if (!success) {
- toast.error(message);
- return;
- }
+ // Update the laboratories query
+ queryClient.setQueryData(
+ ["course-laboratories", courseUUID],
+ (oldData: LaboratoryBaseInfo[]) => {
+ return [...oldData, newLaboratory];
+ }
+ );
- setLoading(false);
- toast.success("The laboratory has been created successfully");
- closeDialogCallback();
+ // Show a success toast
+ toast.success("The laboratory has been created successfully");
- laboratoriesDispatcher({
- type: courseLaboratoriesActionType.ADD_LABORATORY,
- payload: {
- laboratory: {
- uuid: laboratoryUUID,
- name: values.name,
- opening_date: values.openingDate,
- due_date: values.dueDate
- }
- }
+ // Close the dialog
+ closeDialogCallback();
+ },
+ onSettled: () => {
+ setIsCreatingLab(false);
+ }
+ });
+
+ const onSubmit = async (values: z.infer) => {
+ createLaboratoryMutation({
+ courseUUID,
+ ...values
});
};
@@ -169,7 +187,7 @@ export const CreateLaboratoryForm = ({
)}
/>
-
diff --git a/src/screens/course-page/participants/CourseParticipants.tsx b/src/screens/course-page/participants/CourseParticipants.tsx
index 65778772..658c4945 100644
--- a/src/screens/course-page/participants/CourseParticipants.tsx
+++ b/src/screens/course-page/participants/CourseParticipants.tsx
@@ -2,8 +2,9 @@ import { Button } from "@/components/ui/button";
import { DataTableColumnHeader } from "@/components/ui/data-table-column-header";
import { getEnrolledStudentsService } from "@/services/courses/get-enrolled-students.service";
import { EnrolledStudent } from "@/types/entities/general-entities";
+import { useQuery } from "@tanstack/react-query";
import { ColumnDef } from "@tanstack/react-table";
-import { useEffect, useMemo, useState } from "react";
+import { useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "sonner";
@@ -15,8 +16,15 @@ export const CourseParticipants = () => {
const navigate = useNavigate();
// Data state
- const [isLoading, setIsLoading] = useState(true);
- const [students, setStudents] = useState([]);
+ const {
+ data: students,
+ isLoading,
+ isError,
+ error
+ } = useQuery({
+ queryKey: ["course-students", courseUUID],
+ queryFn: () => getEnrolledStudentsService(courseUUID)
+ });
// Table state
const tableColumns = useMemo[]>(
@@ -44,32 +52,17 @@ export const CourseParticipants = () => {
[]
);
- useEffect(() => {
- getStudents();
- }, []);
-
- const getStudents = async () => {
- const { success, ...response } =
- await getEnrolledStudentsService(courseUUID);
- if (!success) {
- toast.error(response.message);
- navigate("/courses");
- }
-
- setStudents(response.students);
- setIsLoading(false);
- };
-
- // State modifiers
- const addStudent = (student: EnrolledStudent) => {
- setStudents((prev) => [...prev, student]);
- };
+ // Error handling
+ if (isError) {
+ toast.error(error?.message);
+ navigate(`/courses/${courseUUID}/laboratories`);
+ }
return (
Enrolled students
-
+
[];
- students: EnrolledStudent[];
+ students: EnrolledStudent[] | undefined;
}
export const CourseParticipantsTable = ({
@@ -24,5 +24,8 @@ export const CourseParticipantsTable = ({
);
}
+ // TODO: Show the error component if the students data is not available
+ if (!students) return null;
+
return ;
};
diff --git a/src/screens/course-page/participants/components/FoundStudentCard.tsx b/src/screens/course-page/participants/components/FoundStudentCard.tsx
new file mode 100644
index 00000000..dc2eb9f4
--- /dev/null
+++ b/src/screens/course-page/participants/components/FoundStudentCard.tsx
@@ -0,0 +1,94 @@
+import { Button } from "@/components/ui/button";
+import { enrollStudentService } from "@/services/courses/enroll-student.service";
+import { EnrolledStudent, Student } from "@/types/entities/general-entities";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useParams } from "react-router-dom";
+import { toast } from "sonner";
+
+interface FoundStudentCardProps {
+ student: Student;
+}
+
+export const FoundStudentCard = ({ student }: FoundStudentCardProps) => {
+ const { courseUUID = "" } = useParams<{ courseUUID: string }>();
+
+ const queryClient = useQueryClient();
+
+ const { mutate: enrollStudentMutation } = useMutation({
+ mutationFn: (data: EnrolledStudent) => {
+ return enrollStudentService(data, courseUUID);
+ },
+ onMutate: async (data: EnrolledStudent) => {
+ // Cancel any outgoing refetches
+ await queryClient.cancelQueries({
+ queryKey: ["course-students", courseUUID]
+ });
+
+ // Keep the current value
+ const previousStudents = queryClient.getQueryData([
+ "course-students",
+ courseUUID
+ ]);
+
+ // Optimistically update the list of students
+ queryClient.setQueryData(
+ ["course-students", courseUUID],
+ (old) => {
+ return old ? [...old, data] : [data];
+ }
+ );
+
+ // Return the previous value
+ return { previousStudents };
+ },
+ onSuccess: () => {
+ // Show a success toast
+ toast.success("Student enrolled successfully");
+ },
+ onError: (error) => {
+ // Show an error toast
+ toast.error(error.message);
+
+ // Rollback to the previous value
+ queryClient.setQueryData(
+ ["course-students", courseUUID],
+ (old) => {
+ return old ? old : [];
+ }
+ );
+ },
+ onSettled: () => {
+ // Invalidate the query to refetch the data
+ queryClient.invalidateQueries({
+ queryKey: ["course-students", courseUUID],
+ exact: true
+ });
+ }
+ });
+
+ const enrollStudent = async () => {
+ const studentData = {
+ uuid: student.uuid,
+ full_name: student.full_name,
+ institutional_id: student.institutional_id,
+ is_active: true
+ };
+
+ enrollStudentMutation(studentData);
+ };
+
+ return (
+
+
+ {student.full_name}
+
+
+ {student.institutional_id}
+
+
+ );
+};
diff --git a/src/components/FoundStudentCard/FoundStudentsSkeleton.tsx b/src/screens/course-page/participants/components/FoundStudentsSkeleton.tsx
similarity index 69%
rename from src/components/FoundStudentCard/FoundStudentsSkeleton.tsx
rename to src/screens/course-page/participants/components/FoundStudentsSkeleton.tsx
index bb245b7a..a237e93c 100644
--- a/src/components/FoundStudentCard/FoundStudentsSkeleton.tsx
+++ b/src/screens/course-page/participants/components/FoundStudentsSkeleton.tsx
@@ -1,5 +1,5 @@
-import { ScrollArea } from "../ui/scroll-area";
-import { Skeleton } from "../ui/skeleton";
+import { ScrollArea } from "../../../../components/ui/scroll-area";
+import { Skeleton } from "../../../../components/ui/skeleton";
export const FoundStudentsSkeleton = () => {
return (
diff --git a/src/screens/course-page/participants/dialogs/enroll-student/EnrollStudentDialog.tsx b/src/screens/course-page/participants/dialogs/enroll-student/EnrollStudentDialog.tsx
index 985ebe29..05145065 100644
--- a/src/screens/course-page/participants/dialogs/enroll-student/EnrollStudentDialog.tsx
+++ b/src/screens/course-page/participants/dialogs/enroll-student/EnrollStudentDialog.tsx
@@ -1,18 +1,11 @@
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
-import { EnrolledStudent } from "@/types/entities/general-entities";
import { DialogDescription, DialogTitle } from "@radix-ui/react-dialog";
import { PlusCircle } from "lucide-react";
import { EnrollStudentForm } from "./EnrollStudentForm";
-interface EnrollStudentDialogProps {
- addStudentCallback: (student: EnrolledStudent) => void;
-}
-
-export const EnrollStudentDialog = ({
- addStudentCallback
-}: EnrollStudentDialogProps) => {
+export const EnrollStudentDialog = () => {
return (
);
diff --git a/src/screens/course-page/participants/dialogs/enroll-student/EnrollStudentForm.tsx b/src/screens/course-page/participants/dialogs/enroll-student/EnrollStudentForm.tsx
index 7f68f181..a193fe25 100644
--- a/src/screens/course-page/participants/dialogs/enroll-student/EnrollStudentForm.tsx
+++ b/src/screens/course-page/participants/dialogs/enroll-student/EnrollStudentForm.tsx
@@ -1,21 +1,16 @@
-import { FoundStudentCard } from "@/components/FoundStudentCard/FoundStudentCard";
-import { FoundStudentsSkeleton } from "@/components/FoundStudentCard/FoundStudentsSkeleton";
import { EmptyContentText } from "@/components/Texts/EmptyContentText";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import useDebounce from "@/hooks/useDebounce";
+import { FoundStudentsSkeleton } from "@/screens/course-page/participants/components/FoundStudentsSkeleton";
import { searchStudentByFullNameService } from "@/services/accounts/search-student-by-fullname.service";
-import { EnrolledStudent, Student } from "@/types/entities/general-entities";
+import { Student } from "@/types/entities/general-entities";
import { Fragment, useEffect, useState } from "react";
import { toast } from "sonner";
-interface EnrollStudentFormProps {
- addStudentCallback: (student: EnrolledStudent) => void;
-}
+import { FoundStudentCard } from "../../components/FoundStudentCard";
-export const EnrollStudentForm = ({
- addStudentCallback
-}: EnrollStudentFormProps) => {
+export const EnrollStudentForm = () => {
// Search state
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState("");
@@ -61,10 +56,7 @@ export const EnrollStudentForm = ({
{students.map((student) => (
-
+
))}
diff --git a/src/screens/courses-list/CoursesHome.tsx b/src/screens/courses-list/CoursesHome.tsx
index 8e565fa7..b49dcb45 100644
--- a/src/screens/courses-list/CoursesHome.tsx
+++ b/src/screens/courses-list/CoursesHome.tsx
@@ -1,18 +1,18 @@
-import { CourseCard } from "@/components/CourseCard/CourseCard";
-import { CourseCardSkeleton } from "@/components/CourseCard/CourseCardSkeleton";
import { GridContainer } from "@/components/GridContainer";
import { EmptyContentText } from "@/components/Texts/EmptyContentText";
import { AuthContext } from "@/context/AuthContext";
-import { UserCoursesContext } from "@/context/courses/UserCoursesContext";
+import { useUserCourses } from "@/hooks/courses/useCourses";
import { SessionRole } from "@/hooks/useSession";
import { useContext } from "react";
+import { CourseCard } from "./components/CourseCard";
+import { CourseCardSkeleton } from "./components/CourseCardSkeleton";
import { CreateCourseDialog } from "./dialogs/create-course/CreateCourseDialog";
import { JoinCourseDialog } from "./dialogs/join-course/JoinCourseDialog";
import { RenameCourseDialog } from "./dialogs/rename-course/RenameCourseDialog";
export const CoursesHome = () => {
- const { isLoading, userCourses } = useContext(UserCoursesContext);
+ const { isLoading, userCourses } = useUserCourses();
const { user } = useContext(AuthContext);
const role = user?.role || "student";
@@ -37,7 +37,7 @@ export const CoursesHome = () => {
? Array.from({ length: 3 }).map((_, i) => (
))
- : userCourses.courses.map((course) => {
+ : userCourses?.courses.map((course) => {
return (
{
{/* "Accordion element" to show the hidden courses*/}
Hidden courses
- {userCourses.hiddenCourses.length > 0 ? (
+ {(userCourses?.hiddenCourses.length || 0) > 0 ? (
- {userCourses.hiddenCourses.map((course) => (
+ {userCourses?.hiddenCourses.map((course) => (
))}
diff --git a/src/components/CourseCard/ButtonIconContainer.tsx b/src/screens/courses-list/components/ButtonIconContainer.tsx
similarity index 100%
rename from src/components/CourseCard/ButtonIconContainer.tsx
rename to src/screens/courses-list/components/ButtonIconContainer.tsx
diff --git a/src/components/CourseCard/CourseCard.tsx b/src/screens/courses-list/components/CourseCard.tsx
similarity index 100%
rename from src/components/CourseCard/CourseCard.tsx
rename to src/screens/courses-list/components/CourseCard.tsx
diff --git a/src/components/CourseCard/CourseCardSkeleton.tsx b/src/screens/courses-list/components/CourseCardSkeleton.tsx
similarity index 86%
rename from src/components/CourseCard/CourseCardSkeleton.tsx
rename to src/screens/courses-list/components/CourseCardSkeleton.tsx
index 81be9b64..0ec32b7c 100644
--- a/src/components/CourseCard/CourseCardSkeleton.tsx
+++ b/src/screens/courses-list/components/CourseCardSkeleton.tsx
@@ -1,4 +1,4 @@
-import { Skeleton } from "../ui/skeleton";
+import { Skeleton } from "@/components/ui/skeleton";
export const CourseCardSkeleton = () => {
return (
diff --git a/src/components/CourseCard/CourseDropDown.tsx b/src/screens/courses-list/components/CourseDropDown.tsx
similarity index 62%
rename from src/components/CourseCard/CourseDropDown.tsx
rename to src/screens/courses-list/components/CourseDropDown.tsx
index a3dbc525..bbdce574 100644
--- a/src/components/CourseCard/CourseDropDown.tsx
+++ b/src/screens/courses-list/components/CourseDropDown.tsx
@@ -7,13 +7,14 @@ import {
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { AuthContext } from "@/context/AuthContext";
-import { UserCoursesContext } from "@/context/courses/UserCoursesContext";
-import { CoursesActionType } from "@/hooks/courses/coursesReducer";
+import { UserCoursesDialogsContext } from "@/context/courses/UserCoursesDialogsContext";
+import { CoursesState } from "@/hooks/courses/useCourses";
import { SessionRole } from "@/hooks/useSession";
import { getInvitationCodeService } from "@/services/courses/get-invitation-code.service";
import { toggleCourseVisibilityService } from "@/services/courses/toggle-course-visibility.service";
import { Course } from "@/types/entities/general-entities";
import { copyToClipboard } from "@/utils/utils";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
ClipboardCopy,
Eye,
@@ -30,11 +31,66 @@ interface CourseDropDownProps {
}
export const CourseDropDown = ({ course, isHidden }: CourseDropDownProps) => {
- const { userCoursesDispatcher, openRenameCourseDialog } =
- useContext(UserCoursesContext);
+ const { openRenameCourseDialog } = useContext(UserCoursesDialogsContext);
+
const { user } = useContext(AuthContext);
const role = user?.role || "student";
+ // Courses mutations
+ const queryClient = useQueryClient();
+ const { mutate: toggleCourseVisibilityMutation } = useMutation({
+ mutationFn: toggleCourseVisibilityService,
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (data) => {
+ const { visible } = data;
+
+ // Show a success toast
+ toast.success(
+ visible ? "Course shown successfully" : "Course hidden successfully"
+ );
+
+ // Update courses query
+ queryClient.setQueryData(
+ ["courses"],
+ (oldData: CoursesState | undefined) => {
+ if (!oldData) return oldData;
+
+ if (visible) {
+ // Remove the course from the hidden courses list
+ const hiddenCourses = oldData.hiddenCourses.filter(
+ (hiddenCourse) => hiddenCourse.uuid != course.uuid
+ );
+
+ // Add the course to the courses list
+ const courses = [...oldData.courses, course];
+
+ return {
+ ...oldData,
+ courses,
+ hiddenCourses
+ };
+ } else {
+ // Remove the course from the courses list
+ const courses = oldData.courses.filter(
+ (visibleCourse) => visibleCourse.uuid != course.uuid
+ );
+
+ // Add the course to the hidden courses list
+ const hiddenCourses = [...oldData.hiddenCourses, course];
+
+ return {
+ ...oldData,
+ courses,
+ hiddenCourses
+ };
+ }
+ }
+ );
+ }
+ });
+
const getDropdownOptionsByRole = (role: SessionRole) => {
if (role == "teacher") {
return [
@@ -58,6 +114,7 @@ export const CourseDropDown = ({ course, isHidden }: CourseDropDownProps) => {
const { success, ...response } = await getInvitationCodeService(
course.uuid
);
+
if (!success) {
toast.error(response.message);
return;
@@ -76,41 +133,9 @@ export const CourseDropDown = ({ course, isHidden }: CourseDropDownProps) => {
}
};
- const hideCourse = async () => {
- const { success, message, visible } = await toggleCourseVisibilityService(
- course.uuid
- );
- if (!success || visible) {
- toast.error(message);
- return;
- }
-
- toast.success(message);
- userCoursesDispatcher({
- type: CoursesActionType.HIDE_COURSE,
- payload: {
- uuid: course.uuid
- }
- });
- };
+ const hideCourse = () => toggleCourseVisibilityMutation(course.uuid);
- const showCourse = async () => {
- const { success, message, visible } = await toggleCourseVisibilityService(
- course.uuid
- );
- if (!success || !visible) {
- toast.error(message);
- return;
- }
-
- toast.success(message);
- userCoursesDispatcher({
- type: CoursesActionType.SHOW_COURSE,
- payload: {
- uuid: course.uuid
- }
- });
- };
+ const showCourse = () => toggleCourseVisibilityMutation(course.uuid);
return (
diff --git a/src/screens/courses-list/dialogs/create-course/CreateCourseDialog.tsx b/src/screens/courses-list/dialogs/create-course/CreateCourseDialog.tsx
index c2116e04..f7970836 100644
--- a/src/screens/courses-list/dialogs/create-course/CreateCourseDialog.tsx
+++ b/src/screens/courses-list/dialogs/create-course/CreateCourseDialog.tsx
@@ -1,4 +1,3 @@
-import { ButtonIconContainer } from "@/components/CourseCard/ButtonIconContainer";
import {
Dialog,
DialogContent,
@@ -10,6 +9,7 @@ import {
import { Plus } from "lucide-react";
import { useState } from "react";
+import { ButtonIconContainer } from "../../components/ButtonIconContainer";
import { CreateCourseForm } from "./CreateCourseForm";
export const CreateCourseDialog = () => {
diff --git a/src/screens/courses-list/dialogs/create-course/CreateCourseForm.tsx b/src/screens/courses-list/dialogs/create-course/CreateCourseForm.tsx
index 2db56435..99bc3625 100644
--- a/src/screens/courses-list/dialogs/create-course/CreateCourseForm.tsx
+++ b/src/screens/courses-list/dialogs/create-course/CreateCourseForm.tsx
@@ -9,11 +9,11 @@ import {
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
-import { UserCoursesContext } from "@/context/courses/UserCoursesContext";
-import { CoursesActionType } from "@/hooks/courses/coursesReducer";
+import { CoursesState } from "@/hooks/courses/useCourses";
import { createCourseService } from "@/services/courses/create-course.service";
import { zodResolver } from "@hookform/resolvers/zod";
-import { useContext, useState } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod";
@@ -33,8 +33,8 @@ interface CreateCourseFormProps {
export const CreateCourseForm = ({
closeDialogCallback
}: CreateCourseFormProps) => {
- const { userCoursesDispatcher } = useContext(UserCoursesContext);
- const [state, setState] = useState<"idle" | "loading">("idle");
+ // Form state
+ const [isCreatingCourse, setIsCreatingCourse] = useState(false);
const form = useForm>({
resolver: zodResolver(CreateCourseSchema),
defaultValues: {
@@ -42,33 +42,45 @@ export const CreateCourseForm = ({
}
});
- const formSubmitCallback = async (
- values: z.infer
- ) => {
- createCourse(values.name);
- };
+ // Create course mutation
+ const queryClient = useQueryClient();
+ const { mutate: createCourseMutation } = useMutation({
+ mutationFn: createCourseService,
+ onMutate: () => {
+ setIsCreatingCourse(true);
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (creationResponse, name: string) => {
+ const newCourse = {
+ ...creationResponse,
+ name
+ };
- const createCourse = async (name: string) => {
- setState("loading");
+ // Show a success toast
+ toast.success("The course was created successfully");
- const { success, ...response } = await createCourseService(name);
- if (!success) {
- toast.error(response.message);
- setState("idle");
- return;
- }
+ // Update courses query
+ queryClient.setQueryData(["courses"], (oldCourses: CoursesState) => {
+ return {
+ ...oldCourses,
+ courses: [...oldCourses.courses, newCourse]
+ };
+ });
- const { message, course } = response;
- toast.success(message);
- setState("idle");
+ // Close dialog
+ closeDialogCallback();
+ },
+ onSettled: () => {
+ setIsCreatingCourse(false);
+ }
+ });
- userCoursesDispatcher({
- type: CoursesActionType.ADD_COURSE,
- payload: {
- course
- }
- });
- closeDialogCallback();
+ const formSubmitCallback = async (
+ values: z.infer
+ ) => {
+ await createCourseMutation(values.name);
};
return (
@@ -96,7 +108,7 @@ export const CreateCourseForm = ({
)}
>
-
+
Create
diff --git a/src/screens/courses-list/dialogs/join-course/JoinCourseDialog.tsx b/src/screens/courses-list/dialogs/join-course/JoinCourseDialog.tsx
index 2c9fb2e6..f7a75f19 100644
--- a/src/screens/courses-list/dialogs/join-course/JoinCourseDialog.tsx
+++ b/src/screens/courses-list/dialogs/join-course/JoinCourseDialog.tsx
@@ -1,4 +1,3 @@
-import { ButtonIconContainer } from "@/components/CourseCard/ButtonIconContainer";
import {
Dialog,
DialogContent,
@@ -10,6 +9,7 @@ import {
import { LogIn } from "lucide-react";
import { useState } from "react";
+import { ButtonIconContainer } from "../../components/ButtonIconContainer";
import { JoinCourseForm } from "./JoinCourseForm";
export const JoinCourseDialog = () => {
diff --git a/src/screens/courses-list/dialogs/join-course/JoinCourseForm.tsx b/src/screens/courses-list/dialogs/join-course/JoinCourseForm.tsx
index b9a46480..04b50fd8 100644
--- a/src/screens/courses-list/dialogs/join-course/JoinCourseForm.tsx
+++ b/src/screens/courses-list/dialogs/join-course/JoinCourseForm.tsx
@@ -9,11 +9,11 @@ import {
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
-import { UserCoursesContext } from "@/context/courses/UserCoursesContext";
-import { CoursesActionType } from "@/hooks/courses/coursesReducer";
+import { CoursesState } from "@/hooks/courses/useCourses";
import { joinUsingInvitationCodeService } from "@/services/courses/join-using-invitation-code.service";
import { zodResolver } from "@hookform/resolvers/zod";
-import { useContext, useState } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod";
@@ -32,8 +32,8 @@ interface JoinCourseFormProps {
export const JoinCourseForm = ({
closeDialogCallback
}: JoinCourseFormProps) => {
- const { userCoursesDispatcher } = useContext(UserCoursesContext);
- const [state, setState] = useState<"idle" | "loading">("idle");
+ // Form state
+ const [isJoiningToCourse, setIsJoiningToCourse] = useState(false);
const form = useForm>({
resolver: zodResolver(JoinCourseSchema),
defaultValues: {
@@ -41,40 +41,39 @@ export const JoinCourseForm = ({
}
});
- const formSubmitCallback = async (
- values: z.infer
- ) => {
- joinCourse(values.invitationCode);
- };
+ // Join course mutation
+ const queryClient = useQueryClient();
+ const { mutate: joinCourseMutation } = useMutation({
+ mutationFn: joinUsingInvitationCodeService,
+ onMutate: () => {
+ setIsJoiningToCourse(true);
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (joinResponse) => {
+ const course = joinResponse;
- const joinCourse = async (code: string) => {
- setState("loading");
+ // Update courses query
+ queryClient.setQueryData(["courses"], (oldCourses: CoursesState) => {
+ return {
+ ...oldCourses,
+ courses: [...oldCourses.courses, course]
+ };
+ });
- const { success, ...response } = await joinUsingInvitationCodeService(code);
- if (!success) {
- toast.error(response.message);
- setState("idle");
- return;
- }
+ // Show a success message
+ toast.success("You have joined the course");
- const { course } = response;
- if (!course) {
- toast.error("Unable to get the course data");
- setState("idle");
- return;
+ closeDialogCallback();
+ },
+ onSettled: () => {
+ setIsJoiningToCourse(false);
}
+ });
- const { message } = response;
- toast.success(message);
- setState("idle");
-
- userCoursesDispatcher({
- type: CoursesActionType.ADD_COURSE,
- payload: {
- course
- }
- });
- closeDialogCallback();
+ const formSubmitCallback = (values: z.infer) => {
+ joinCourseMutation(values.invitationCode);
};
return (
@@ -102,7 +101,7 @@ export const JoinCourseForm = ({
)}
>
-
+
Join
diff --git a/src/screens/courses-list/dialogs/rename-course/RenameCourseDialog.tsx b/src/screens/courses-list/dialogs/rename-course/RenameCourseDialog.tsx
index 2244ddba..dc301c09 100644
--- a/src/screens/courses-list/dialogs/rename-course/RenameCourseDialog.tsx
+++ b/src/screens/courses-list/dialogs/rename-course/RenameCourseDialog.tsx
@@ -5,14 +5,15 @@ import {
DialogHeader,
DialogTitle
} from "@/components/ui/dialog";
-import { UserCoursesContext } from "@/context/courses/UserCoursesContext";
+import { UserCoursesDialogsContext } from "@/context/courses/UserCoursesDialogsContext";
import { useContext } from "react";
import { RenameCourseForm } from "./RenameCourseForm";
export const RenameCourseDialog = () => {
- const { renameCourseDialogState, closeRenameCourseDialog } =
- useContext(UserCoursesContext);
+ const { renameCourseDialogState, closeRenameCourseDialog } = useContext(
+ UserCoursesDialogsContext
+ );
const { isOpen, selectedCourse } = renameCourseDialogState;
const isOpenWithCourseSelected = isOpen && selectedCourse !== null;
diff --git a/src/screens/courses-list/dialogs/rename-course/RenameCourseForm.tsx b/src/screens/courses-list/dialogs/rename-course/RenameCourseForm.tsx
index e6ad28a7..6f5d743d 100644
--- a/src/screens/courses-list/dialogs/rename-course/RenameCourseForm.tsx
+++ b/src/screens/courses-list/dialogs/rename-course/RenameCourseForm.tsx
@@ -9,10 +9,11 @@ import {
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
-import { UserCoursesContext } from "@/context/courses/UserCoursesContext";
-import { CoursesActionType } from "@/hooks/courses/coursesReducer";
+import { UserCoursesDialogsContext } from "@/context/courses/UserCoursesDialogsContext";
+import { CoursesState } from "@/hooks/courses/useCourses";
import { renameCourseService } from "@/services/courses/rename-course.service";
import { zodResolver } from "@hookform/resolvers/zod";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useContext, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -27,14 +28,12 @@ const RenameCourseSchema = z.object({
});
export const RenameCourseForm = () => {
- const {
- userCoursesDispatcher,
- renameCourseDialogState,
- closeRenameCourseDialog
- } = useContext(UserCoursesContext);
+ const { renameCourseDialogState, closeRenameCourseDialog } = useContext(
+ UserCoursesDialogsContext
+ );
const course = renameCourseDialogState.selectedCourse;
- const [state, setState] = useState<"idle" | "loading">("idle");
+ const [isUpdating, setIsUpdating] = useState(false);
const form = useForm>({
resolver: zodResolver(RenameCourseSchema),
defaultValues: {
@@ -42,35 +41,54 @@ export const RenameCourseForm = () => {
}
});
+ // Rename course mutation
+ const queryClient = useQueryClient();
+ const { mutate: renameMutation } = useMutation({
+ mutationFn: (name: string) => renameCourseService(course!.uuid, name),
+ onMutate: () => setIsUpdating(true),
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (_, name: string) => {
+ // Show a success toast
+ toast.success("Course renamed successfully");
+
+ // Update courses query
+ queryClient.setQueryData(
+ ["courses"],
+ (oldData: CoursesState | undefined) => {
+ if (!oldData) return oldData;
+
+ return {
+ ...oldData,
+ courses: oldData.courses.map((course) => {
+ if (course.uuid != renameCourseDialogState.selectedCourse?.uuid)
+ return course;
+
+ return {
+ ...course,
+ name
+ };
+ })
+ };
+ }
+ );
+
+ // Close dialog
+ closeRenameCourseDialog();
+ },
+ onSettled: () => {
+ setIsUpdating(false);
+ }
+ });
+
// Prevent form to be shown if there is no course selected
if (!course) return null;
const formSubmitCallback = async (
values: z.infer
) => {
- RenameCourse(values.name);
- };
-
- const RenameCourse = async (name: string) => {
- setState("loading");
-
- const { success, message } = await renameCourseService(course.uuid, name);
- if (!success) {
- toast.error(message);
- setState("idle");
- return;
- }
-
- userCoursesDispatcher({
- type: CoursesActionType.RENAME_COURSE,
- payload: {
- uuid: course.uuid,
- name
- }
- });
- toast.success("Course renamed successfully");
- setState("idle");
- closeRenameCourseDialog();
+ renameMutation(values.name);
};
return (
@@ -98,7 +116,7 @@ export const RenameCourseForm = () => {
)}
>
-
+
Rename
diff --git a/src/screens/edit-laboratory/EditLaboratory.tsx b/src/screens/edit-laboratory/EditLaboratory.tsx
index 6669b99f..7da70c3d 100644
--- a/src/screens/edit-laboratory/EditLaboratory.tsx
+++ b/src/screens/edit-laboratory/EditLaboratory.tsx
@@ -4,6 +4,11 @@ import { EditLaboratoryContext } from "@/context/laboratories/EditLaboratoryCont
import { EditLaboratoryActionType } from "@/hooks/laboratories/editLaboratoryTypes";
import { EditLaboratoryPageSkeleton } from "@/screens/edit-laboratory/skeletons/EditLaboratoryPageSkeleton";
import { createMarkdownBlockService } from "@/services/laboratories/add-markdown-block.service";
+import {
+ Laboratory,
+ MarkdownBlock
+} from "@/types/entities/laboratory-entities";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { TextCursor } from "lucide-react";
import { Suspense, useContext } from "react";
import { lazily } from "react-lazily";
@@ -31,29 +36,53 @@ export const EditLaboratory = () => {
// Url params
const { laboratoryUUID } = useParams<{ laboratoryUUID: string }>();
- // Handlers
- const handleAddTextBlock = async () => {
- const { success, message, uuid } = await createMarkdownBlockService(
- laboratoryUUID as string
- );
- if (!success) {
- toast.error(message);
- return;
+ // Create markdown block mutation
+ const queryClient = useQueryClient();
+ const { mutate: createMarkdownBlockMutation } = useMutation({
+ mutationFn: createMarkdownBlockService,
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (createdBlockUUID) => {
+ // Update the global state
+ laboratoryStateDispatcher({
+ type: EditLaboratoryActionType.ADD_MARKDOWN_BLOCK,
+ payload: {
+ uuid: createdBlockUUID
+ }
+ });
+
+ // Update the laboratory query
+ const newMarkdownBlock: MarkdownBlock = {
+ uuid: createdBlockUUID,
+ content: "",
+ index: laboratory!.blocks.length,
+ blockType: "markdown"
+ };
+
+ queryClient.setQueryData(
+ ["laboratory", laboratoryUUID],
+ (oldData: Laboratory) => {
+ return {
+ ...oldData,
+ blocks: [...oldData.blocks, newMarkdownBlock]
+ };
+ }
+ );
+
+ // Show success message
+ toast.success("The new markdown block has been created successfully");
}
+ });
- toast.success(message);
- laboratoryStateDispatcher({
- type: EditLaboratoryActionType.ADD_MARKDOWN_BLOCK,
- payload: {
- uuid
- }
- });
+ const handleAddTextBlock = () => {
+ createMarkdownBlockMutation(laboratoryUUID!);
};
- if (!laboratory) return null;
-
if (loading) return ;
+ if (!laboratory) return null;
+
return (
{/* Header to update base laboratory details */}
diff --git a/src/screens/edit-laboratory/components/LaboratoryDetails.tsx b/src/screens/edit-laboratory/components/LaboratoryDetails.tsx
index 8c1cf729..dadfdb38 100644
--- a/src/screens/edit-laboratory/components/LaboratoryDetails.tsx
+++ b/src/screens/edit-laboratory/components/LaboratoryDetails.tsx
@@ -2,11 +2,14 @@ import { EditLaboratoryContext } from "@/context/laboratories/EditLaboratoryCont
import { EditLaboratoryActionType } from "@/hooks/laboratories/editLaboratoryTypes";
import { updateLaboratoryDetailsService } from "@/services/laboratories/update-laboratory-details.service";
import { getTeacherRubricsService } from "@/services/rubrics/get-teacher-rubrics.service";
-import { LaboratoryBaseInfo } from "@/types/entities/laboratory-entities";
-import { CreatedRubric } from "@/types/entities/rubric-entities";
+import {
+ Laboratory,
+ LaboratoryBaseInfo
+} from "@/types/entities/laboratory-entities";
import { zodResolver } from "@hookform/resolvers/zod";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Save } from "lucide-react";
-import { useContext, useEffect, useState } from "react";
+import { useContext, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod";
@@ -91,50 +94,79 @@ export const LaboratoryDetails = ({
}
});
- const handleSubmit = async (data: z.infer) => {
- setIsUpdating(true);
-
- const { success, message } = await updateLaboratoryDetailsService({
- laboratoryUUID: laboratoryDetails.uuid,
- name: data.name,
- due_date: data.dueDate,
- opening_date: data.openingDate,
- rubric_uuid: data.rubricUUID
- });
+ // Update laboratory details mutation
+ const queryClient = useQueryClient();
+ const { mutate: updateLaboratoryMutation } = useMutation({
+ mutationFn: updateLaboratoryDetailsService,
+ onMutate: () => {
+ setIsUpdating(true);
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (_, { name, opening_date, due_date, rubric_uuid }) => {
+ const openingDateWithDefaultTimeZone = `${opening_date}:00Z`;
+ const dueDateWithDefaultTimeZone = `${due_date}:00Z`;
- if (!success) {
- toast.error(message);
- } else {
- toast.success(message);
+ // Update laboratory state
laboratoryStateDispatcher({
type: EditLaboratoryActionType.UPDATE_LABORATORY_DATA,
payload: {
- name: data.name,
- due_date: data.dueDate,
- opening_date: data.openingDate,
- rubricUUID: data.rubricUUID
+ name,
+ opening_date: openingDateWithDefaultTimeZone,
+ due_date: dueDateWithDefaultTimeZone,
+ rubricUUID: rubric_uuid
}
});
+
+ // Show success message
+ toast.success("Laboratory details updated successfully");
+
+ // Update laboratory query
+ queryClient.setQueryData(
+ ["laboratory", laboratoryDetails.uuid],
+ (oldData: Laboratory) => {
+ return {
+ // Keep the UUID and blocks
+ ...oldData,
+ name,
+ opening_date: openingDateWithDefaultTimeZone,
+ due_date: dueDateWithDefaultTimeZone,
+ rubric_uuid
+ };
+ }
+ );
+ },
+ onSettled: () => {
+ setIsUpdating(false);
}
+ });
+
+ const handleSubmit = async (data: z.infer) => {
+ const { name, rubricUUID, openingDate, dueDate } = data;
- setIsUpdating(false);
+ updateLaboratoryMutation({
+ laboratoryUUID: laboratoryDetails.uuid,
+ name,
+ due_date: dueDate,
+ opening_date: openingDate,
+ rubric_uuid: rubricUUID
+ });
};
// Rubrics state
- const [rubrics, setRubrics] = useState([]);
- useEffect(() => {
- const fetchTeacherRubrics = async () => {
- const { success, rubrics, message } = await getTeacherRubricsService();
- if (!success) {
- toast.error(message);
- return;
- }
-
- setRubrics(rubrics);
- };
+ const {
+ data: rubrics,
+ isError: isTeacherRubricsError,
+ error: TeacherRubricsError
+ } = useQuery({
+ queryKey: ["rubrics"],
+ queryFn: getTeacherRubricsService
+ });
- fetchTeacherRubrics();
- }, []);
+ if (isTeacherRubricsError) {
+ toast.error(TeacherRubricsError.message);
+ }
return (
diff --git a/src/screens/edit-rubric/dialogs/DeleteCriteriaDialog.tsx b/src/screens/edit-rubric/dialogs/DeleteCriteriaDialog.tsx
index c7265dda..855648ed 100644
--- a/src/screens/edit-rubric/dialogs/DeleteCriteriaDialog.tsx
+++ b/src/screens/edit-rubric/dialogs/DeleteCriteriaDialog.tsx
@@ -10,10 +10,18 @@ import {
} from "@/components/ui/alert-dialog";
import { deleteCriteriaService } from "@/services/rubrics/delete-criteria.service";
import { useEditRubricModalsStore } from "@/stores/edit-rubric-modals-store";
-import { useEditRubricStore } from "@/stores/edit-rubric-store";
+import { Rubric } from "@/types/entities/rubric-entities";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
-export const DeleteCriteriaDialog = () => {
+interface DeleteCriteriaDialogProps {
+ rubricUUID: string;
+}
+
+export const DeleteCriteriaDialog = ({
+ rubricUUID
+}: DeleteCriteriaDialogProps) => {
+ // Dialog state
const {
isDeleteCriteriaModalOpen,
setIsDeleteCriteriaModalOpen,
@@ -21,26 +29,42 @@ export const DeleteCriteriaDialog = () => {
setSelectedCriteriaUUID
} = useEditRubricModalsStore();
- const { deleteCriteria } = useEditRubricStore();
+ // Delete criteria mutation
+ const queryClient = useQueryClient();
- if (!selectedCriteriaUUID) return null;
+ const { mutate: deleteCriteriaMutation } = useMutation({
+ mutationFn: (criteriaUUID: string) => deleteCriteriaService(criteriaUUID),
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (_ctx, criteriaUUID: string) => {
+ // Show a success toast
+ toast.success("Criteria deleted successfully");
- const handleCancel = () => {
- setSelectedCriteriaUUID(undefined);
- };
+ // Update rubric query
+ queryClient.setQueryData(["rubric", rubricUUID], (oldData: Rubric) => {
+ return {
+ ...oldData,
+ objectives: oldData.objectives?.map((objective) => {
+ return {
+ ...objective,
+ criteria: objective.criteria?.filter(
+ (criteria) => criteria.uuid !== criteriaUUID
+ )
+ };
+ })
+ };
+ });
- const handleProceed = async () => {
- // Send request to delete criteria
- const { success, message } =
- await deleteCriteriaService(selectedCriteriaUUID);
- if (!success) {
- toast.error(message);
- return;
+ // Update modals state
+ handleCloseDialog();
}
+ });
+
+ if (!selectedCriteriaUUID) return null;
- // Update modal state and show confirmation alert
- toast.success(message);
- deleteCriteria(selectedCriteriaUUID);
+ const handleCloseDialog = () => {
+ setIsDeleteCriteriaModalOpen(false);
setSelectedCriteriaUUID(undefined);
};
@@ -60,8 +84,14 @@ export const DeleteCriteriaDialog = () => {
- Cancel
- Proceed
+
+ Cancel
+
+ deleteCriteriaMutation(selectedCriteriaUUID)}
+ >
+ Proceed
+
diff --git a/src/screens/edit-rubric/dialogs/DeleteObjectiveDialog.tsx b/src/screens/edit-rubric/dialogs/DeleteObjectiveDialog.tsx
index 4611af85..1eb09b5c 100644
--- a/src/screens/edit-rubric/dialogs/DeleteObjectiveDialog.tsx
+++ b/src/screens/edit-rubric/dialogs/DeleteObjectiveDialog.tsx
@@ -10,10 +10,18 @@ import {
} from "@/components/ui/alert-dialog";
import { deleteObjectiveService } from "@/services/rubrics/delete-objective.service";
import { useEditRubricModalsStore } from "@/stores/edit-rubric-modals-store";
-import { useEditRubricStore } from "@/stores/edit-rubric-store";
+import { Rubric } from "@/types/entities/rubric-entities";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
-export const DeleteObjectiveDialog = () => {
+interface DeleteObjectiveDialogProps {
+ rubricUUID: string;
+}
+
+export const DeleteObjectiveDialog = ({
+ rubricUUID
+}: DeleteObjectiveDialogProps) => {
+ // Modal state
const {
isDeleteObjectiveModalOpen,
setIsDeleteObjectiveModalOpen,
@@ -21,27 +29,36 @@ export const DeleteObjectiveDialog = () => {
setSelectedObjectiveUUID
} = useEditRubricModalsStore();
- const { deleteObjective } = useEditRubricStore();
+ // Delete objective mutation
+ const queryClient = useQueryClient();
+ const { mutate: deleteObjectiveMutation } = useMutation({
+ mutationFn: deleteObjectiveService,
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (_ctx, objectiveUUID: string) => {
+ // Show a success toast
+ toast.success("Objective deleted successfully");
- if (!selectedObjectiveUUID) return null;
+ // Update rubric query
+ queryClient.setQueryData(["rubric", rubricUUID], (oldData: Rubric) => {
+ return {
+ ...oldData,
+ objectives: oldData.objectives?.filter(
+ (objective) => objective.uuid !== objectiveUUID
+ )
+ };
+ });
- const handleCancel = () => {
- setSelectedObjectiveUUID(undefined);
- };
-
- const handleProceed = async () => {
- // Send request to delete the objective
- const { success, message } = await deleteObjectiveService(
- selectedObjectiveUUID
- );
- if (!success) {
- toast.error(message);
- return;
+ // Update modals state
+ handleCloseDialog();
}
+ });
+
+ if (!selectedObjectiveUUID) return null;
- // Update modal state and show confirmation alert
- toast.success(message);
- deleteObjective(selectedObjectiveUUID);
+ const handleCloseDialog = () => {
+ setIsDeleteObjectiveModalOpen(false);
setSelectedObjectiveUUID(undefined);
};
@@ -62,8 +79,14 @@ export const DeleteObjectiveDialog = () => {
- Cancel
- Proceed
+
+ Cancel
+
+ deleteObjectiveMutation(selectedObjectiveUUID)}
+ >
+ Proceed
+
diff --git a/src/screens/edit-rubric/dialogs/add-criteria/AddCriteriaDialog.tsx b/src/screens/edit-rubric/dialogs/add-criteria/AddCriteriaDialog.tsx
index 22410fe7..fd86ea35 100644
--- a/src/screens/edit-rubric/dialogs/add-criteria/AddCriteriaDialog.tsx
+++ b/src/screens/edit-rubric/dialogs/add-criteria/AddCriteriaDialog.tsx
@@ -1,4 +1,3 @@
-import { ActionButton } from "@/components/Rubric/ActionButton";
import {
Dialog,
DialogContent,
@@ -7,17 +6,22 @@ import {
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
+import { ActionButton } from "@/screens/edit-rubric/components/ActionButton";
import { useState } from "react";
import { AddCriteriaForm } from "./AddCriteriaForm";
+interface AddCriteriaDialogProps {
+ rubricUUID: string;
+ objectiveUUID: string;
+ objectiveIndex: number;
+}
+
export const AddCriteriaDialog = ({
+ rubricUUID,
objectiveUUID,
- index
-}: {
- objectiveUUID: string;
- index: number;
-}) => {
+ objectiveIndex
+}: AddCriteriaDialogProps) => {
const [isOpen, setIsOpen] = useState(false);
const closeDialog = () => setIsOpen(false);
@@ -26,7 +30,7 @@ export const AddCriteriaDialog = ({
setIsOpen(true)}
/>
@@ -39,6 +43,7 @@ export const AddCriteriaDialog = ({
diff --git a/src/screens/edit-rubric/dialogs/add-criteria/AddCriteriaForm.tsx b/src/screens/edit-rubric/dialogs/add-criteria/AddCriteriaForm.tsx
index 66c1f8ca..85cea503 100644
--- a/src/screens/edit-rubric/dialogs/add-criteria/AddCriteriaForm.tsx
+++ b/src/screens/edit-rubric/dialogs/add-criteria/AddCriteriaForm.tsx
@@ -11,14 +11,16 @@ import {
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { addCriteriaToObjectiveService } from "@/services/rubrics/add-criteria-to-objective.service";
-import { useEditRubricStore } from "@/stores/edit-rubric-store";
+import { Rubric } from "@/types/entities/rubric-entities";
import { zodResolver } from "@hookform/resolvers/zod";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod";
interface addCriteriaFormProps {
+ rubricUUID: string;
objectiveUUID: string;
closeDialogCallback: () => void;
}
@@ -35,12 +37,10 @@ const criteriaSchema = z.object({
});
export const AddCriteriaForm = ({
+ rubricUUID,
objectiveUUID,
closeDialogCallback
}: addCriteriaFormProps) => {
- // Rubric global state
- const { addCriteria } = useEditRubricStore();
-
// Form state
const [loading, setLoading] = useState(false);
const form = useForm>({
@@ -51,28 +51,70 @@ export const AddCriteriaForm = ({
}
});
- const onSubmit = async (data: z.infer) => {
- setLoading(true);
- await handleAddCriteria(data.description, data.weight);
- setLoading(false);
+ // Add criteria mutation
+ const queryClient = useQueryClient();
+
+ type AddCriteriaMutationFnArgs = {
+ description: string;
+ weight: number;
};
- const handleAddCriteria = async (description: string, weight: number) => {
- const { success, message, uuid } = await addCriteriaToObjectiveService(
- objectiveUUID,
- description,
- weight
- );
+ const { mutate: addCriteriaMutation } = useMutation({
+ mutationFn: ({ description, weight }: AddCriteriaMutationFnArgs) =>
+ addCriteriaToObjectiveService({
+ objectiveUUID,
+ description,
+ weight
+ }),
+ onMutate: ({ description, weight }: AddCriteriaMutationFnArgs) => {
+ setLoading(true);
+
+ // Forwards the description to the following callbacks
+ return { description, weight };
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (
+ newCriteriaUUID: string,
+ { description, weight }: AddCriteriaMutationFnArgs
+ ) => {
+ // Show a success toast
+ toast.success("Criteria added successfully");
+
+ // Update rubric query
+ queryClient.setQueryData(["rubric", rubricUUID], (oldData: Rubric) => {
+ const newCriteria = {
+ uuid: newCriteriaUUID,
+ description,
+ weight
+ };
- if (!success) {
- toast.error(message);
- return;
+ return {
+ ...oldData,
+ objectives: oldData.objectives.map((objective) => {
+ if (objective.uuid !== objectiveUUID) {
+ return objective;
+ }
+
+ return {
+ ...objective,
+ criteria: [...objective.criteria, newCriteria]
+ };
+ })
+ };
+ });
+
+ // Update modals state
+ closeDialogCallback();
+ },
+ onSettled: () => {
+ setLoading(false);
}
+ });
- const newCriteria = { uuid, description, weight };
- addCriteria(objectiveUUID, newCriteria);
- toast.success("The criteria has been added!");
- closeDialogCallback();
+ const onSubmit = async (data: z.infer) => {
+ addCriteriaMutation(data);
};
return (
diff --git a/src/screens/edit-rubric/dialogs/add-objective/AddObjectiveDialog.tsx b/src/screens/edit-rubric/dialogs/add-objective/AddObjectiveDialog.tsx
index d9c4c739..a5cf49df 100644
--- a/src/screens/edit-rubric/dialogs/add-objective/AddObjectiveDialog.tsx
+++ b/src/screens/edit-rubric/dialogs/add-objective/AddObjectiveDialog.tsx
@@ -1,4 +1,3 @@
-import { ActionButton } from "@/components/Rubric/ActionButton";
import {
Dialog,
DialogContent,
@@ -7,18 +6,19 @@ import {
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
-import { useEditRubricStore } from "@/stores/edit-rubric-store";
+import { ActionButton } from "@/screens/edit-rubric/components/ActionButton";
import { useState } from "react";
import { AddObjectiveForm } from "./AddObjectiveForm";
-export const AddObjectiveDialog = () => {
+interface AddObjectiveDialogProps {
+ rubricUUID: string;
+}
+
+export const AddObjectiveDialog = ({ rubricUUID }: AddObjectiveDialogProps) => {
const [isOpen, setIsOpen] = useState(false);
const closeDialog = () => setIsOpen(false);
- const { rubric } = useEditRubricStore();
- if (!rubric) return null;
-
return (
);
diff --git a/src/screens/rubrics-list/dialogs/CreateRubricForm.tsx b/src/screens/rubrics-list/dialogs/CreateRubricForm.tsx
index 572d714f..154f932b 100644
--- a/src/screens/rubrics-list/dialogs/CreateRubricForm.tsx
+++ b/src/screens/rubrics-list/dialogs/CreateRubricForm.tsx
@@ -12,6 +12,7 @@ import { Input } from "@/components/ui/input";
import { createRubricService } from "@/services/rubrics/create-rubric.service";
import { CreatedRubric } from "@/types/entities/rubric-entities";
import { zodResolver } from "@hookform/resolvers/zod";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -27,14 +28,13 @@ const CreateRubricSchema = z.object({
interface CreateRubricFormProps {
closeDialogCallback: () => void;
- addRubricCallback: (rubric: CreatedRubric) => void;
}
export const CreateRubricForm = ({
- closeDialogCallback,
- addRubricCallback
+ closeDialogCallback
}: CreateRubricFormProps) => {
- const [state, setState] = useState<"idle" | "loading">("idle");
+ // Form state
+ const [isCreatingRubric, setIsCreatingRubric] = useState(false);
const form = useForm>({
resolver: zodResolver(CreateRubricSchema),
defaultValues: {
@@ -42,28 +42,43 @@ export const CreateRubricForm = ({
}
});
- const formSubmitCallback = async (
- values: z.infer
- ) => {
- setState("loading");
- createRubric(values.name);
- };
+ // Create rubric mutation
+ const queryClient = useQueryClient();
+ const { mutate: createRubricMutation } = useMutation({
+ mutationFn: createRubricService,
+ onMutate: () => {
+ setIsCreatingRubric(true);
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ onSuccess: (newRubricUUID: string, rubricName: string) => {
+ // Update the rubrics state
+ queryClient.setQueryData(["rubrics"], (oldData: CreatedRubric[]) => {
+ const newRubric = {
+ uuid: newRubricUUID,
+ name: rubricName
+ };
+
+ return [...oldData, newRubric];
+ });
- const createRubric = async (name: string) => {
- const { success, ...response } = await createRubricService(name);
- if (!success) {
- toast.error(response.message);
- setState("idle");
- return;
+ // Close the dialog
+ closeDialogCallback();
+
+ // Show a toast
+ toast.success("The rubric has been created!");
+ },
+ onSettled: () => {
+ setIsCreatingRubric(false);
}
+ });
- setState("idle");
- addRubricCallback({
- uuid: response.uuid,
- name: name
- });
- closeDialogCallback();
- toast.success("The rubric has been created!");
+ const formSubmitCallback = async (
+ values: z.infer
+ ) => {
+ const { name } = values;
+ createRubricMutation(name);
};
return (
@@ -91,7 +106,7 @@ export const CreateRubricForm = ({
)}
>
-
+
Create
diff --git a/src/services/accounts/get-registered-admins.service.ts b/src/services/accounts/get-registered-admins.service.ts
index 7d697c39..9309b98d 100644
--- a/src/services/accounts/get-registered-admins.service.ts
+++ b/src/services/accounts/get-registered-admins.service.ts
@@ -1,6 +1,6 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
export type registeredAdminDTO = {
uuid: string;
@@ -9,29 +9,24 @@ export type registeredAdminDTO = {
created_at: string;
};
-type registeredAdminsResponse = GenericResponse & {
- admins: registeredAdminDTO[];
-};
-
-export const getRegisteredAdminsService =
- async (): Promise => {
- const { axios } = HttpRequester.getInstance();
+export async function getRegisteredAdminsService(): Promise<
+ registeredAdminDTO[]
+> {
+ const { axios } = HttpRequester.getInstance();
- try {
- const { data } = await axios.get("/accounts/admins");
- return {
- success: true,
- message: "Admins were obtained successfully",
- admins: data["admins"]
- };
- } catch (error) {
- let errorMessage = "There was an error";
+ try {
+ const { data } = await axios.get("/accounts/admins");
+ return data["admins"];
+ } catch (error) {
+ const DEFAULT_ERROR_MESSAGE = "We had an error obtaining the admins list";
- if (error instanceof AxiosError) {
- const { message } = error.response?.data || "";
- if (message) errorMessage = message;
- }
-
- return { success: false, message: errorMessage, admins: [] };
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
+ if (error instanceof AxiosError) {
+ const { message } = error.response?.data || "";
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- };
+
+ throw new Error(errorMessage);
+ }
+}
diff --git a/src/services/blocks/delete-markdown-block.service.ts b/src/services/blocks/delete-markdown-block.service.ts
index e67e44b7..2cefab3e 100644
--- a/src/services/blocks/delete-markdown-block.service.ts
+++ b/src/services/blocks/delete-markdown-block.service.ts
@@ -1,30 +1,24 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-export const deleteMarkdownBlockService = async (
+export async function deleteMarkdownBlockService(
markdownBlockUUID: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
await axios.delete(`/blocks/markdown_blocks/${markdownBlockUUID}`);
-
- return {
- success: true,
- message: "The markdown block has been deleted successfully"
- };
} catch (error) {
- let errorMessage = "There was an error deleting the markdown block";
+ const DEFAULT_ERROR_MESSAGE =
+ "There was an error deleting the markdown block";
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
if (message) errorMessage = message;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/blocks/delete-test-block.service.ts b/src/services/blocks/delete-test-block.service.ts
index afec3944..599fcea9 100644
--- a/src/services/blocks/delete-test-block.service.ts
+++ b/src/services/blocks/delete-test-block.service.ts
@@ -1,30 +1,23 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-export const deleteTestBlockService = async (
+export async function deleteTestBlockService(
testBlockUUID: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
await axios.delete(`/blocks/test_blocks/${testBlockUUID}`);
-
- return {
- success: true,
- message: "The test block has been deleted successfully"
- };
} catch (error) {
- let errorMessage = "There was an error deleting the test block";
+ const DEFAULT_ERROR_MESSAGE = "There was an error deleting the test block";
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
if (message) errorMessage = message;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/blocks/update-markdown-block-content.service.ts b/src/services/blocks/update-markdown-block-content.service.ts
index c27918b9..23e2e171 100644
--- a/src/services/blocks/update-markdown-block-content.service.ts
+++ b/src/services/blocks/update-markdown-block-content.service.ts
@@ -1,38 +1,32 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
type updateMarkdownBlockContentParams = {
markdownBlockUUID: string;
content: string;
};
-export const updateMarkdownBlockContentService = async ({
+export async function updateMarkdownBlockContentService({
markdownBlockUUID,
content
-}: updateMarkdownBlockContentParams): Promise => {
+}: updateMarkdownBlockContentParams): Promise {
const { axios } = HttpRequester.getInstance();
try {
await axios.patch(`/blocks/markdown_blocks/${markdownBlockUUID}/content`, {
content
});
-
- return {
- success: true,
- message: "The markdown block has been updated successfully"
- };
} catch (error) {
- let errorMessage = "There was an error updating the markdown block";
+ const DEFAULT_ERROR_MESSAGE =
+ "There was an error updating the markdown block";
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
if (message) errorMessage = message;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/blocks/update-test-block.service.ts b/src/services/blocks/update-test-block.service.ts
new file mode 100644
index 00000000..e5e90cbf
--- /dev/null
+++ b/src/services/blocks/update-test-block.service.ts
@@ -0,0 +1,40 @@
+import { AxiosError } from "axios";
+
+import { HttpRequester } from "../axios";
+
+type updateTestBlockRequest = {
+ blockUUID: string;
+ blockLanguageUUID: string;
+ blockName: string;
+ blockTestArchive?: File;
+};
+
+export async function updateTestBlockService({
+ blockUUID,
+ blockLanguageUUID,
+ blockName,
+ blockTestArchive
+}: updateTestBlockRequest): Promise {
+ const { axios } = HttpRequester.getInstance();
+
+ try {
+ // Create the multipart form data
+ const formData = new FormData();
+ formData.append("block_name", blockName);
+ formData.append("language_uuid", blockLanguageUUID);
+ if (blockTestArchive) formData.append("test_archive", blockTestArchive);
+
+ // Send the request
+ await axios.put(`/blocks/test_blocks/${blockUUID}`, formData);
+ } catch (error) {
+ const DEFAULT_ERROR_MESSAGE = "There was an error updating the test block";
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
+
+ if (error instanceof AxiosError) {
+ const { message } = error.response?.data || "";
+ if (message) errorMessage = message;
+ }
+
+ throw new Error(errorMessage);
+ }
+}
diff --git a/src/services/courses/create-course.service.ts b/src/services/courses/create-course.service.ts
index a743311f..30d84906 100644
--- a/src/services/courses/create-course.service.ts
+++ b/src/services/courses/create-course.service.ts
@@ -3,36 +3,22 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type CreateCourseResponse = {
- success: boolean;
- message: string;
- course: Course;
-};
-
-export const createCourseService = async (
- name: string
-): Promise => {
+export async function createCourseService(name: string): Promise {
const { axios } = HttpRequester.getInstance();
try {
const { data } = await axios.post("/courses", { name });
- return {
- success: true,
- message: "Course was created successfully",
- course: data
- };
+ return data;
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "We had a problem creating the course";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- course: {} as Course
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/courses/enroll-student.service.ts b/src/services/courses/enroll-student.service.ts
index bfb5a3b5..fab61ba6 100644
--- a/src/services/courses/enroll-student.service.ts
+++ b/src/services/courses/enroll-student.service.ts
@@ -1,37 +1,28 @@
+import { EnrolledStudent } from "@/types/entities/general-entities";
import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type EnrollStudentResponse = {
- success: boolean;
- message: string;
-};
-
export const enrollStudentService = async (
- courseId: string,
- studentId: string
-): Promise => {
+ student: EnrolledStudent,
+ courseUUID: string
+): Promise => {
const { axios } = HttpRequester.getInstance();
try {
- await axios.post(`/courses/${courseId}/students`, {
- student_uuid: studentId
+ await axios.post(`/courses/${courseUUID}/students`, {
+ student_uuid: student.uuid
});
- return {
- success: true,
- message: "Student was enrolled successfully"
- };
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "We had an error enrolling the student";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
};
diff --git a/src/services/courses/get-course.service.ts b/src/services/courses/get-course.service.ts
index e416d006..5eee5b90 100644
--- a/src/services/courses/get-course.service.ts
+++ b/src/services/courses/get-course.service.ts
@@ -3,36 +3,21 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type GetCourseResponse = {
- success: boolean;
- message: string;
- course: Course;
-};
-
-export const getCourseService = async (
- id: string
-): Promise => {
+export async function getCourseService(id: string): Promise {
const { axios } = HttpRequester.getInstance();
try {
const { data } = await axios.get(`/courses/${id}`);
- return {
- success: true,
- message: "Course information was retrieved successfully",
- course: data
- };
+ return data;
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "There was an error obtaining the course";
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
if (message) errorMessage = message;
}
- return {
- success: false,
- message: errorMessage,
- course: {} as Course
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/courses/get-enrolled-students.service.ts b/src/services/courses/get-enrolled-students.service.ts
index 960fee41..fd21c36d 100644
--- a/src/services/courses/get-enrolled-students.service.ts
+++ b/src/services/courses/get-enrolled-students.service.ts
@@ -3,36 +3,24 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type GetEnrolledStudentsResponse = {
- success: boolean;
- message: string;
- students: EnrolledStudent[];
-};
-
export const getEnrolledStudentsService = async (
- id: string
-): Promise => {
+ courseUUID: string
+): Promise => {
const { axios } = HttpRequester.getInstance();
try {
- const { data } = await axios.get(`/courses/${id}/students`);
- return {
- success: true,
- message: "Students were retrieved successfully",
- students: data.students
- };
+ const { data } = await axios.get(`/courses/${courseUUID}/students`);
+ return data.students;
} catch (error) {
- let errorMessage = "There was an error retrieving the students";
+ const DEFAULT_ERROR_MESSAGE = "We had an error obtaining the students";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- students: []
- };
+ throw new Error(errorMessage);
}
};
diff --git a/src/services/courses/get-user-courses.service.ts b/src/services/courses/get-user-courses.service.ts
index 84a782a0..6bea072a 100644
--- a/src/services/courses/get-user-courses.service.ts
+++ b/src/services/courses/get-user-courses.service.ts
@@ -3,37 +3,28 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type GetCoursesREsponse = {
- success: boolean;
- message: string;
+type GetCoursesResponse = {
courses: Course[];
hiddenCourses: Course[];
};
-export const getCoursesService = async (): Promise => {
- const { axios } = HttpRequester.getInstance();
-
+export const getCoursesService = async (): Promise => {
try {
- const { data } = await axios.get("/courses");
+ const { data } = await HttpRequester.getInstance().axios.get("/courses");
return {
- success: true,
- message: "Courses were obtained successfully",
courses: data["courses"],
hiddenCourses: data["hidden_courses"]
};
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "We had an error obtaining your courses";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- courses: [],
- hiddenCourses: []
- };
+ throw new Error(errorMessage);
}
};
diff --git a/src/services/courses/join-using-invitation-code.service.ts b/src/services/courses/join-using-invitation-code.service.ts
index c5659047..e1111c9c 100644
--- a/src/services/courses/join-using-invitation-code.service.ts
+++ b/src/services/courses/join-using-invitation-code.service.ts
@@ -3,36 +3,24 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type JoinUsingInvitationCodeResponse = {
- success: boolean;
- message: string;
- course?: Course;
-};
-
-export const joinUsingInvitationCodeService = async (
+export async function joinUsingInvitationCodeService(
code: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
- const response = await axios.post(`/courses/join/${code}`);
-
- return {
- success: true,
- message: "You have joined the course",
- course: response.data.course
- };
+ const { data } = await axios.post(`/courses/join/${code}`);
+ return data.course;
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "We had a problem adding you to the course";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/courses/rename-course.service.ts b/src/services/courses/rename-course.service.ts
index 9707ce35..5f125aa6 100644
--- a/src/services/courses/rename-course.service.ts
+++ b/src/services/courses/rename-course.service.ts
@@ -2,34 +2,24 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type RenameCourseResponse = {
- success: boolean;
- message: string;
-};
-
export const renameCourseService = async (
id: string,
name: string
-): Promise => {
+): Promise => {
const { axios } = HttpRequester.getInstance();
try {
await axios.patch(`/courses/${id}/name`, { name });
- return {
- success: true,
- message: "Course was renamed successfully"
- };
} catch (error) {
- let errorMessage = "There was an error renaming the course";
+ const DEFAULT_ERROR_MESSAGE = "There was an error renaming the course";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
};
diff --git a/src/services/courses/toggle-course-visibility.service.ts b/src/services/courses/toggle-course-visibility.service.ts
index 3c4640c3..9cf23a88 100644
--- a/src/services/courses/toggle-course-visibility.service.ts
+++ b/src/services/courses/toggle-course-visibility.service.ts
@@ -2,36 +2,30 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type ToggleCourseVisibilityResponse = {
- success: boolean;
- message: string;
+type ToggleCourseVisibilityNewResponse = {
visible: boolean;
};
export const toggleCourseVisibilityService = async (
courseId: string
-): Promise => {
+): Promise => {
const { axios } = HttpRequester.getInstance();
try {
const { data } = await axios.patch(`/courses/${courseId}/visibility`);
return {
- success: true,
- message: "Course visibility was updated successfully",
visible: data.visible
};
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "There was an error";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- visible: false
- };
+ throw new Error(errorMessage);
}
};
diff --git a/src/services/laboratories/add-markdown-block.service.ts b/src/services/laboratories/add-markdown-block.service.ts
index 749c622c..fa8b58aa 100644
--- a/src/services/laboratories/add-markdown-block.service.ts
+++ b/src/services/laboratories/add-markdown-block.service.ts
@@ -1,38 +1,27 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-type createMarkdownBlockResponse = GenericResponse & {
- uuid: string;
-};
-
-export const createMarkdownBlockService = async (
+export async function createMarkdownBlockService(
laboratoryUUID: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
const { data } = await axios.post(
`/laboratories/markdown_blocks/${laboratoryUUID}`
);
-
- return {
- success: true,
- message: "The new markdown block has been created successfully",
- uuid: data.uuid
- };
+ return data.uuid;
} catch (error) {
- let errorMessage = "There was an error creating the new markdown block";
+ const DEFAULT_ERROR_MESSAGE =
+ "We had an error creating the new markdown block";
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- uuid: ""
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/laboratories/add-test-block.service.ts b/src/services/laboratories/add-test-block.service.ts
index 299d849e..b592d9e2 100644
--- a/src/services/laboratories/add-test-block.service.ts
+++ b/src/services/laboratories/add-test-block.service.ts
@@ -1,24 +1,20 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-type createTestBlockResponse = GenericResponse & {
- uuid: string;
-};
-
-type createTestBlockRequest = {
+type createTestBlockParams = {
laboratoryUUID: string;
blockLanguageUUID: string;
blockName: string;
blockTestArchive: File;
};
-export const createTestBlockService = async ({
+export async function createTestBlockService({
blockLanguageUUID,
laboratoryUUID,
blockName,
blockTestArchive
-}: createTestBlockRequest): Promise => {
+}: createTestBlockParams): Promise {
const { axios } = HttpRequester.getInstance();
try {
@@ -34,24 +30,17 @@ export const createTestBlockService = async ({
formData
);
- // Parse the request
- return {
- success: true,
- message: "The new test block has been created successfully",
- uuid: data.uuid
- };
+ return data.uuid;
} catch (error) {
- let errorMessage = "There was an error creating the new test block";
+ const DEFAULT_ERROR_MESSAGE =
+ "There was an error creating the new test block";
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
if (message) errorMessage = message;
}
- return {
- success: false,
- message: errorMessage,
- uuid: ""
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/laboratories/create-laboratory.service.ts b/src/services/laboratories/create-laboratory.service.ts
index 00724f36..907644a5 100644
--- a/src/services/laboratories/create-laboratory.service.ts
+++ b/src/services/laboratories/create-laboratory.service.ts
@@ -1,10 +1,6 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
-
-type createLaboratoryResponse = GenericResponse & {
- laboratoryUUID: string;
-};
+import { HttpRequester } from "../axios";
interface createLaboratoryServiceParams {
courseUUID: string;
@@ -13,12 +9,12 @@ interface createLaboratoryServiceParams {
dueDate: string;
}
-export const createLaboratoryService = async ({
+export async function createLaboratoryService({
courseUUID,
name,
openingDate,
dueDate
-}: createLaboratoryServiceParams): Promise => {
+}: createLaboratoryServiceParams): Promise {
const { axios } = HttpRequester.getInstance();
try {
@@ -29,23 +25,17 @@ export const createLaboratoryService = async ({
due_date: dueDate
});
- return {
- success: true,
- message: "Laboratory was created successfully",
- laboratoryUUID: data.uuid
- };
+ return data.uuid;
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "There was an error creating the laboratory";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- laboratoryUUID: ""
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/laboratories/get-course-laboratories.service.ts b/src/services/laboratories/get-course-laboratories.service.ts
index 92603c32..a8fc3ba7 100644
--- a/src/services/laboratories/get-course-laboratories.service.ts
+++ b/src/services/laboratories/get-course-laboratories.service.ts
@@ -1,37 +1,27 @@
import { LaboratoryBaseInfo } from "@/types/entities/laboratory-entities";
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-type getCourseLaboratoriesResponse = GenericResponse & {
- laboratories: LaboratoryBaseInfo[];
-};
-
-export const getCourseLaboratoriesService = async (
+export async function getCourseLaboratoriesService(
courseUUID: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
const { data } = await axios.get(`/courses/${courseUUID}/laboratories`);
-
- return {
- success: true,
- message: "Laboratories were fetched successfully",
- laboratories: data.laboratories
- };
+ return data.laboratories;
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE =
+ "There was an error obtaining the laboratories";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- laboratories: []
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/laboratories/get-laboratory-by-uuid.service.ts b/src/services/laboratories/get-laboratory-by-uuid.service.ts
index a73564d2..461db96c 100644
--- a/src/services/laboratories/get-laboratory-by-uuid.service.ts
+++ b/src/services/laboratories/get-laboratory-by-uuid.service.ts
@@ -5,15 +5,26 @@ import {
} from "@/types/entities/laboratory-entities";
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-type getLaboratoryByUUIDResponse = GenericResponse & {
- laboratory: Laboratory | null;
+type markdownBlockBackendResponse = {
+ uuid: string;
+ content: string;
+ index: number;
};
-export const getLaboratoryByUUIDService = async (
+type testBlockBackendResponse = {
+ uuid: string;
+ language_uuid: string;
+ test_archive_uuid: string;
+ submission_uuid: string;
+ name: string;
+ index: number;
+};
+
+export async function getLaboratoryByUUIDService(
laboratoryUUID: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
@@ -29,10 +40,9 @@ export const getLaboratoryByUUIDService = async (
blocks: []
};
- // Parse blocks
+ // Unify blocks in a single array
const markdownBlocks: MarkdownBlock[] = data.markdown_blocks.map(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (block: any) => ({
+ (block: markdownBlockBackendResponse) => ({
uuid: block.uuid,
content: block.content,
index: block.index,
@@ -42,8 +52,7 @@ export const getLaboratoryByUUIDService = async (
laboratory.blocks.push(...markdownBlocks);
const testBlocks: TestBlock[] = data.test_blocks.map(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (block: any) => ({
+ (block: testBlockBackendResponse) => ({
uuid: block.uuid,
languageUUID: block.language_uuid,
testArchiveUUID: block.test_archive_uuid,
@@ -55,7 +64,7 @@ export const getLaboratoryByUUIDService = async (
);
laboratory.blocks.push(...testBlocks);
- // Sort blocks by index and replace the index with the array index
+ // Sort blocks by index and replace their index with their index in the array
laboratory.blocks
.sort((a, b) => a.index - b.index)
.map((block, index) => ({
@@ -63,23 +72,17 @@ export const getLaboratoryByUUIDService = async (
index
}));
- return {
- success: true,
- message: "Laboratory data has been fetched successfully",
- laboratory
- };
+ return laboratory;
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "There was an error obtaining the laboratory";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- laboratory: null
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/laboratories/get-students-progress.service.ts b/src/services/laboratories/get-students-progress.service.ts
new file mode 100644
index 00000000..5a311292
--- /dev/null
+++ b/src/services/laboratories/get-students-progress.service.ts
@@ -0,0 +1,12 @@
+import { LaboratoryProgressReport } from "@/types/entities/laboratory-entities";
+
+import { HttpRequester } from "../axios";
+
+export async function getStudentsProgressInLaboratory(
+ laboratoryUUID: string
+): Promise {
+ const { axios } = HttpRequester.getInstance();
+
+ const { data } = await axios.get(`/laboratories/${laboratoryUUID}/progress`);
+ return data;
+}
diff --git a/src/services/laboratories/update-laboratory-details.service.ts b/src/services/laboratories/update-laboratory-details.service.ts
index 4462844e..b3d50a76 100644
--- a/src/services/laboratories/update-laboratory-details.service.ts
+++ b/src/services/laboratories/update-laboratory-details.service.ts
@@ -1,6 +1,6 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
type updateLaboratoryDetailsParams = {
laboratoryUUID: string;
@@ -10,15 +10,13 @@ type updateLaboratoryDetailsParams = {
due_date: string;
};
-type updateLaboratoryDetailsResponse = GenericResponse;
-
-export const updateLaboratoryDetailsService = async ({
+export async function updateLaboratoryDetailsService({
laboratoryUUID,
name,
- rubric_uuid,
opening_date,
- due_date
-}: updateLaboratoryDetailsParams): Promise => {
+ due_date,
+ rubric_uuid
+}: updateLaboratoryDetailsParams): Promise {
const { axios } = HttpRequester.getInstance();
try {
@@ -28,22 +26,15 @@ export const updateLaboratoryDetailsService = async ({
opening_date,
due_date
});
-
- return {
- success: true,
- message: "The laboratory has been updated successfully"
- };
} catch (error) {
- let errorMessage = "There was an error updating the laboratory";
+ const DEFAULT_ERROR_MESSAGE = "We had an error updating the laboratory";
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/languages/get-supported-languages.service.ts b/src/services/languages/get-supported-languages.service.ts
index 993c3a15..69db8aa2 100644
--- a/src/services/languages/get-supported-languages.service.ts
+++ b/src/services/languages/get-supported-languages.service.ts
@@ -7,6 +7,7 @@ type createMarkdownBlockResponse = GenericResponse & {
languages: Language[];
};
+// TODO: Refactor to be compatible with TanStack Query
export const getSupportedLanguagesService =
async (): Promise => {
const { axios } = HttpRequester.getInstance();
diff --git a/src/services/rubrics/add-criteria-to-objective.service.ts b/src/services/rubrics/add-criteria-to-objective.service.ts
index 0ba8c38d..cd6e4891 100644
--- a/src/services/rubrics/add-criteria-to-objective.service.ts
+++ b/src/services/rubrics/add-criteria-to-objective.service.ts
@@ -2,17 +2,17 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type AddCriteriaToObjectiveResponse = {
- uuid: string;
- success: boolean;
- message: string;
+type AddCriteriaToObjectiveReq = {
+ objectiveUUID: string;
+ description: string;
+ weight: number;
};
-export const addCriteriaToObjectiveService = async (
- objectiveUUID: string,
- description: string,
- weight: number
-): Promise => {
+export async function addCriteriaToObjectiveService({
+ objectiveUUID,
+ description,
+ weight
+}: AddCriteriaToObjectiveReq): Promise {
const { axios } = HttpRequester.getInstance();
try {
@@ -23,23 +23,17 @@ export const addCriteriaToObjectiveService = async (
weight
}
);
- return {
- uuid: data.uuid,
- success: true,
- message: "Criteria was added successfully"
- };
+ return data.uuid;
} catch (error) {
- let errorMessage = "There was an error adding the criteria";
+ const DEFAULT_ERROR_MESSAGE = "There was an error adding the criteria";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- uuid: "",
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/rubrics/add-objective.service.ts b/src/services/rubrics/add-objective.service.ts
index 21e837be..42cefdd4 100644
--- a/src/services/rubrics/add-objective.service.ts
+++ b/src/services/rubrics/add-objective.service.ts
@@ -2,39 +2,28 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-export type AddObjectiveResponse = {
- uuid: string;
- success: boolean;
- message: string;
-};
-
-export const addObjectiveService = async (
+export async function addObjectiveService(
rubricUUID: string,
description: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
const { data } = await axios.post(`/rubrics/${rubricUUID}/objectives`, {
description
});
- return {
- uuid: data.uuid,
- success: true,
- message: "Objective was added successfully"
- };
+
+ return data.uuid;
} catch (error) {
- let errorMessage = "There was an error adding the objective";
+ const DEFAULT_ERROR_MESSAGE = "There was an error adding the objective";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- uuid: "",
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/rubrics/create-rubric.service.ts b/src/services/rubrics/create-rubric.service.ts
index 2d7318bf..ade5fb7f 100644
--- a/src/services/rubrics/create-rubric.service.ts
+++ b/src/services/rubrics/create-rubric.service.ts
@@ -2,36 +2,22 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-export type CreateRubricResponse = {
- uuid: string;
- success: boolean;
- message: string;
-};
-
-export const createRubricService = async (
- name: string
-): Promise => {
+export async function createRubricService(name: string): Promise {
const { axios } = HttpRequester.getInstance();
try {
const { data } = await axios.post("/rubrics", { name });
- return {
- uuid: data.uuid,
- success: true,
- message: "Rubric was created successfully"
- };
+ return data.uuid;
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "There was an error creating the rubric";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- uuid: "",
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/rubrics/delete-criteria.service.ts b/src/services/rubrics/delete-criteria.service.ts
index 8b777791..c55fe8f7 100644
--- a/src/services/rubrics/delete-criteria.service.ts
+++ b/src/services/rubrics/delete-criteria.service.ts
@@ -1,29 +1,24 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-export const deleteCriteriaService = async (
+export async function deleteCriteriaService(
criteriaUUID: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
await axios.delete(`rubrics/criteria/${criteriaUUID}`);
- return {
- success: true,
- message: "The criteria has been deleted successfully"
- };
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "We had an error deleting the criteria";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/rubrics/delete-objective.service.ts b/src/services/rubrics/delete-objective.service.ts
index d20f7992..cba29f24 100644
--- a/src/services/rubrics/delete-objective.service.ts
+++ b/src/services/rubrics/delete-objective.service.ts
@@ -1,29 +1,24 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-export const deleteObjectiveService = async (
+export async function deleteObjectiveService(
objectiveUUID: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
await axios.delete(`rubrics/objectives/${objectiveUUID}`);
- return {
- success: true,
- message: "The objective has been deleted successfully"
- };
} catch (error) {
- let errorMessage = "There was an error deleting the objective";
+ const DEFAULT_ERROR_MESSAGE = "We had an error deleting the objective";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/rubrics/get-rubric-by-uuid.service.ts b/src/services/rubrics/get-rubric-by-uuid.service.ts
index 75e4ced6..edfc142e 100644
--- a/src/services/rubrics/get-rubric-by-uuid.service.ts
+++ b/src/services/rubrics/get-rubric-by-uuid.service.ts
@@ -1,36 +1,24 @@
import { Rubric } from "@/types/entities/rubric-entities";
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-type GetRubricByUuidResponse = GenericResponse & {
- rubric: Rubric;
-};
-
-export const getRubricByUuidService = async (
- uuid: string
-): Promise => {
+export async function getRubricByUUIDService(uuid: string): Promise {
const { axios } = HttpRequester.getInstance();
try {
const { data } = await axios.get(`/rubrics/${uuid}`);
- return {
- success: true,
- message: "Rubric retrieved successfully",
- rubric: data.rubric
- };
+ return data.rubric;
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "We had an error obtaining the rubric";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage,
- rubric: {} as Rubric
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/rubrics/get-teacher-rubrics.service.ts b/src/services/rubrics/get-teacher-rubrics.service.ts
index 45279d98..61819246 100644
--- a/src/services/rubrics/get-teacher-rubrics.service.ts
+++ b/src/services/rubrics/get-teacher-rubrics.service.ts
@@ -3,35 +3,22 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-export type GetTeacherRubricsResponse = {
- success: boolean;
- message: string;
- rubrics: CreatedRubric[];
-};
+export async function getTeacherRubricsService(): Promise {
+ const { axios } = HttpRequester.getInstance();
-export const getTeacherRubricsService =
- async (): Promise => {
- const { axios } = HttpRequester.getInstance();
+ try {
+ const { data } = await axios.get("/rubrics");
+ return data["rubrics"];
+ } catch (error) {
+ const DEFAULT_ERROR_MESSAGE = "We had an error obtaining the rubrics list";
- try {
- const { data } = await axios.get("/rubrics");
- return {
- success: true,
- message: "Rubrics were retrieved successfully",
- rubrics: data.rubrics
- };
- } catch (error) {
- let errorMessage = "There was an error retrieving your rubrics";
-
- if (error instanceof AxiosError) {
- const { message } = error.response?.data || "";
- if (message) errorMessage = message;
- }
-
- return {
- success: false,
- message: errorMessage,
- rubrics: []
- };
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
+ if (error instanceof AxiosError) {
+ const { message } = error.response?.data || "";
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- };
+
+ throw new Error(errorMessage);
+ }
+}
diff --git a/src/services/rubrics/update-criteria.service.ts b/src/services/rubrics/update-criteria.service.ts
index 02fb2cef..04cd46e8 100644
--- a/src/services/rubrics/update-criteria.service.ts
+++ b/src/services/rubrics/update-criteria.service.ts
@@ -1,6 +1,6 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
type updateCriteriaServiceParams = {
criteriaUUID: string;
@@ -8,32 +8,28 @@ type updateCriteriaServiceParams = {
weight: number;
};
-export const updateCriteriaService = async ({
+export async function updateCriteriaService({
criteriaUUID,
description,
weight
-}: updateCriteriaServiceParams): Promise => {
+}: updateCriteriaServiceParams): Promise {
+ const { axios } = HttpRequester.getInstance();
+
try {
- const { axios } = HttpRequester.getInstance();
await axios.put(`/rubrics/criteria/${criteriaUUID}`, {
description,
weight
});
- return {
- success: true,
- message: "The criteria has been updated successfully"
- };
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "There was an error updating the criteria";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/rubrics/update-objective.service.ts b/src/services/rubrics/update-objective.service.ts
index d0388052..1f99d014 100644
--- a/src/services/rubrics/update-objective.service.ts
+++ b/src/services/rubrics/update-objective.service.ts
@@ -1,31 +1,27 @@
import { AxiosError } from "axios";
-import { GenericResponse, HttpRequester } from "../axios";
+import { HttpRequester } from "../axios";
-export const updateObjectiveService = async (
+export async function updateObjectiveService(
objectiveUUID: string,
description: string
-): Promise => {
+): Promise {
+ const { axios } = HttpRequester.getInstance();
+
try {
- const { axios } = HttpRequester.getInstance();
await axios.put(`/rubrics/objectives/${objectiveUUID}`, {
description
});
- return {
- success: true,
- message: "The objective has been updated successfully"
- };
} catch (error) {
- let errorMessage = "There was an error";
+ const DEFAULT_ERROR_MESSAGE = "There was an error updating the objective";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/rubrics/update-rubric-name.service.ts b/src/services/rubrics/update-rubric-name.service.ts
index 746b4959..6d536f2c 100644
--- a/src/services/rubrics/update-rubric-name.service.ts
+++ b/src/services/rubrics/update-rubric-name.service.ts
@@ -2,36 +2,26 @@ import { AxiosError } from "axios";
import { HttpRequester } from "../axios";
-type UpdateRubricNameResponse = {
- success: boolean;
- message: string;
-};
-
-export const updateRubricNameService = async (
+export async function updateRubricNameService(
rubricUUID: string,
name: string
-): Promise => {
+): Promise {
const { axios } = HttpRequester.getInstance();
try {
await axios.patch(`/rubrics/${rubricUUID}/name`, {
name
});
- return {
- success: true,
- message: "Rubric name has been updated successfully"
- };
} catch (error) {
- let errorMessage = "There was an error updating the rubric name";
+ const DEFAULT_ERROR_MESSAGE = "We had an error updating the rubric name";
+ // Try to get the error from the response
+ let errorMessage = DEFAULT_ERROR_MESSAGE;
if (error instanceof AxiosError) {
const { message } = error.response?.data || "";
- if (message) errorMessage = message;
+ errorMessage = message || DEFAULT_ERROR_MESSAGE;
}
- return {
- success: false,
- message: errorMessage
- };
+ throw new Error(errorMessage);
}
-};
+}
diff --git a/src/services/submissions/get-submission-real-time-status.service.ts b/src/services/submissions/get-submission-real-time-status.service.ts
new file mode 100644
index 00000000..130c73db
--- /dev/null
+++ b/src/services/submissions/get-submission-real-time-status.service.ts
@@ -0,0 +1,30 @@
+import { CONSTANTS } from "@/config/constants";
+
+import { GenericResponse } from "../axios";
+
+type getSubmissionRealTimeStatusRespose = GenericResponse & {
+ eventSource?: EventSource;
+};
+
+export async function getSubmissionRealTimeStatusService(
+ testBlockUUID: string
+): Promise {
+ try {
+ // Create the event source
+ const eventSource = new EventSource(
+ `${CONSTANTS.API_BASE_URL}/submissions/${testBlockUUID}/status`,
+ { withCredentials: true }
+ );
+
+ return {
+ success: true,
+ message: "Listening for real time status updates",
+ eventSource
+ };
+ } catch {
+ return {
+ success: false,
+ message: "Unable to listen for real time status updates"
+ };
+ }
+}
diff --git a/src/services/submissions/submit-to-test-block.service.ts b/src/services/submissions/submit-to-test-block.service.ts
new file mode 100644
index 00000000..e7d45716
--- /dev/null
+++ b/src/services/submissions/submit-to-test-block.service.ts
@@ -0,0 +1,49 @@
+import { AxiosError } from "axios";
+
+import { GenericResponse, HttpRequester } from "../axios";
+
+type submitToTestBlockRequest = {
+ testBlockUUID: string;
+ submissionArchive: File;
+};
+
+type submitToTestBlockResponse = GenericResponse & {
+ uuid: string;
+};
+
+export async function submitToTestBlockService({
+ testBlockUUID,
+ submissionArchive
+}: submitToTestBlockRequest): Promise {
+ const { axios } = HttpRequester.getInstance();
+
+ try {
+ // Create the multipart form data
+ const formData = new FormData();
+ formData.append("submission_archive", submissionArchive);
+
+ // Send the request
+ const response = await axios.post(
+ `/submissions/${testBlockUUID}`,
+ formData
+ );
+ return {
+ success: true,
+ message: "Your code has been submitted successfully",
+ uuid: response.data.uuid
+ };
+ } catch (error) {
+ let errorMessage = "There was an error submitting your code";
+
+ if (error instanceof AxiosError) {
+ const { message } = error.response?.data || "";
+ if (message) errorMessage = message;
+ }
+
+ return {
+ success: false,
+ message: errorMessage,
+ uuid: ""
+ };
+ }
+}
diff --git a/src/stores/edit-rubric-store.tsx b/src/stores/edit-rubric-store.tsx
deleted file mode 100644
index c726f5c8..00000000
--- a/src/stores/edit-rubric-store.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { Criteria, Objective, Rubric } from "@/types/entities/rubric-entities";
-import { create } from "zustand";
-
-type EditRubricStore = {
- // Rubric global state
- rubric: Rubric | undefined;
- setRubric: (rubric: Rubric) => void;
- resetRubric: () => void;
-
- // Rubric mutations
- setName: (name: string) => void;
-
- // Objective mutations
- addObjective: (objective: Objective) => void;
- updateObjective: (objectiveUUID: string, description: string) => void;
- deleteObjective: (objectiveUUID: string) => void;
-
- // Criteria mutations
- addCriteria: (objectiveUUID: string, criteria: Criteria) => void;
- updateCriteria: ({
- criteriaUUID,
- weight,
- description
- }: updateCriteriaParams) => void;
- deleteCriteria: (criteriaUUID: string) => void;
-};
-
-type updateCriteriaParams = {
- criteriaUUID: string;
- weight: number;
- description: string;
-};
-
-export const useEditRubricStore = create((set) => ({
- // Rubric global state
- rubric: undefined,
- setRubric: (rubric) => set({ rubric }),
- resetRubric: () => set({ rubric: undefined }),
-
- // Rubric mutations
- setName: (name: string) =>
- set((state) => {
- if (!state.rubric) return state;
-
- return {
- rubric: {
- ...state.rubric,
- name
- }
- };
- }),
-
- // Objective mutations
- addObjective: (objective) =>
- set((state) => {
- if (!state.rubric) return state;
-
- return {
- rubric: {
- ...state.rubric,
- objectives: [...state.rubric.objectives, objective]
- }
- };
- }),
- updateObjective: (uuid, description) => {
- set((state) => {
- if (!state.rubric) return state;
-
- const objectives = state.rubric.objectives.map((objective) => {
- if (objective.uuid === uuid) {
- return {
- ...objective,
- description
- };
- }
-
- return objective;
- });
-
- return {
- rubric: {
- ...state.rubric,
- objectives
- }
- };
- });
- },
- deleteObjective: (uuid) => {
- set((state) => {
- if (!state.rubric) return state;
-
- const objectives = state.rubric.objectives.filter(
- (objective) => objective.uuid !== uuid
- );
-
- return {
- rubric: {
- ...state.rubric,
- objectives
- }
- };
- });
- },
-
- // Criteria mutations
- addCriteria: (objectiveUUID, criteria) =>
- set((state) => {
- if (!state.rubric) return state;
-
- const objectives = state.rubric.objectives.map((objective) => {
- if (objective.uuid === objectiveUUID) {
- return {
- ...objective,
- criteria: [...objective.criteria, criteria]
- };
- }
-
- return objective;
- });
-
- return {
- rubric: {
- ...state.rubric,
- objectives
- }
- };
- }),
- updateCriteria: ({
- criteriaUUID,
- weight,
- description
- }: updateCriteriaParams) => {
- set((state) => {
- if (!state.rubric) return state;
-
- const objectives = state.rubric.objectives.map((objective) => {
- const criteria = objective.criteria.map((criteria) => {
- if (criteria.uuid === criteriaUUID) {
- return {
- ...criteria,
- weight,
- description
- };
- }
-
- return criteria;
- });
-
- return {
- ...objective,
- criteria
- };
- });
-
- return {
- rubric: {
- ...state.rubric,
- objectives
- }
- };
- });
- },
- deleteCriteria: (criteriaUUID) => {
- set((state) => {
- if (!state.rubric) return state;
-
- const objectives = state.rubric.objectives.map((objective) => {
- const criteria = objective.criteria.filter(
- (criteria) => criteria.uuid !== criteriaUUID
- );
-
- return {
- ...objective,
- criteria
- };
- });
-
- return {
- rubric: {
- ...state.rubric,
- objectives
- }
- };
- });
- }
-}));
diff --git a/src/types/entities/laboratory-entities.ts b/src/types/entities/laboratory-entities.ts
index 19335923..c29d140c 100644
--- a/src/types/entities/laboratory-entities.ts
+++ b/src/types/entities/laboratory-entities.ts
@@ -30,3 +30,17 @@ export type Laboratory = LaboratoryBaseInfo & {
rubricUUID: string | null;
blocks: LaboratoryBlock[];
};
+
+export type LaboratoryProgressReport = {
+ total_test_blocks: number;
+ students_progress: StudentProgress[];
+};
+
+export type StudentProgress = {
+ student_uuid: string;
+ student_full_name: string;
+ pending_submissions: number;
+ running_submissions: number;
+ failing_submissions: number;
+ success_submissions: number;
+};
diff --git a/src/types/entities/submission-entities.ts b/src/types/entities/submission-entities.ts
new file mode 100644
index 00000000..f302c25f
--- /dev/null
+++ b/src/types/entities/submission-entities.ts
@@ -0,0 +1,8 @@
+export type submissionStatus = "pending" | "running" | "ready";
+
+export type submissionUpdate = {
+ submissionUUID: string;
+ submissionStatus: submissionStatus;
+ testsPassed: boolean;
+ testsOutput: string;
+};
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 8ec85f20..b0aa99e3 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -1,4 +1,5 @@
import { downloadLanguageTemplateService } from "@/services/languages/download-language-template.service";
+import { submissionUpdate } from "@/types/entities/submission-entities";
import { toast } from "sonner";
export const copyToClipboard = async (text: string): Promise => {
@@ -58,3 +59,14 @@ export async function downloadLanguageTemplate(
fileName: `${languageName.toLowerCase()}-template.zip`
});
}
+
+export function parseSubmissionSSEUpdate(data: string): submissionUpdate {
+ const parsedData = JSON.parse(data);
+
+ return {
+ submissionUUID: parsedData.submission_uuid,
+ submissionStatus: parsedData.submission_status,
+ testsPassed: parsedData.tests_passed,
+ testsOutput: parsedData.tests_output
+ };
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 7372c327..4db2ecb4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -17,6 +17,9 @@ export default {
},
},
extend: {
+ fontFamily: {
+ ibmPlexMono: ["IBM Plex Mono", "monospace", "sans-serif"],
+ },
colors: {
red: {
upb: "hsl(var(--upb-red))",