From 4803adf604a6d5658d66a3458796d879a1584cfd Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Fri, 13 Sep 2024 21:35:11 -0600 Subject: [PATCH] creating assignments --- .../assignments/[assignmentName]/route.ts | 21 ++++- .../[moduleName]/pages/[pageName]/route.ts | 35 +++++--- .../[moduleName]/quizzes/[quizName]/route.ts | 55 ++++++++---- .../[courseName]/modules/ExpandableModule.tsx | 17 ++-- .../[courseName]/modules/NewItemForm.tsx | 83 +++++++++++++++++++ nextjs/src/app/globals.css | 5 ++ nextjs/src/components/ButtonSelect.tsx | 30 +++++++ nextjs/src/components/Modal.tsx | 44 ++++++++++ nextjs/src/components/form/TextInput.tsx | 2 +- .../src/hooks/localCourse/assignmentHooks.ts | 43 ++++++++++ nextjs/src/hooks/localCourse/pageHooks.ts | 38 +++++++++ nextjs/src/hooks/localCourse/quizHooks.ts | 38 +++++++++ .../fileStorage/fileStorageService.ts | 26 +++++- 13 files changed, 400 insertions(+), 37 deletions(-) create mode 100644 nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx create mode 100644 nextjs/src/components/ButtonSelect.tsx create mode 100644 nextjs/src/components/Modal.tsx diff --git a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/[assignmentName]/route.ts b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/[assignmentName]/route.ts index 7c76946..6e3cd08 100644 --- a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/[assignmentName]/route.ts +++ b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/[assignmentName]/route.ts @@ -28,7 +28,26 @@ export const PUT = async ( ) => await withErrorHandling(async () => { const assignment = await request.json(); - await fileStorageService.assignments.updateAssignment( + await fileStorageService.assignments.updateOrCreateAssignment( + courseName, + moduleName, + assignmentName, + assignment + ); + return Response.json({}); + }); + +export const POST = async ( + request: Request, + { + params: { courseName, moduleName, assignmentName }, + }: { + params: { courseName: string; moduleName: string; assignmentName: string }; + } +) => + await withErrorHandling(async () => { + const assignment = await request.json(); + await fileStorageService.assignments.updateOrCreateAssignment( courseName, moduleName, assignmentName, diff --git a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/pages/[pageName]/route.ts b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/pages/[pageName]/route.ts index 094e28c..5f5eb3a 100644 --- a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/pages/[pageName]/route.ts +++ b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/pages/[pageName]/route.ts @@ -16,14 +16,27 @@ export const GET = async ( return Response.json(settings); }); -export const PUT = async ( - request: Request, - { - params: { courseName, moduleName, pageName }, - }: { params: { courseName: string; moduleName: string; pageName: string } } -) => - await withErrorHandling(async () => { - const page = await request.json(); - await fileStorageService.pages.updatePage(courseName, moduleName, pageName, page); - return Response.json({}); - }); + export const PUT = async ( + request: Request, + { + params: { courseName, moduleName, pageName }, + }: { params: { courseName: string; moduleName: string; pageName: string } } + ) => + await withErrorHandling(async () => { + const page = await request.json(); + await fileStorageService.pages.updatePage(courseName, moduleName, pageName, page); + return Response.json({}); + }); + + export const POST = async ( + request: Request, + { + params: { courseName, moduleName, pageName }, + }: { params: { courseName: string; moduleName: string; pageName: string } } + ) => + await withErrorHandling(async () => { + const page = await request.json(); + await fileStorageService.pages.updatePage(courseName, moduleName, pageName, page); + return Response.json({}); + }); + \ No newline at end of file diff --git a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/quizzes/[quizName]/route.ts b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/quizzes/[quizName]/route.ts index f7afbfc..25da7fc 100644 --- a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/quizzes/[quizName]/route.ts +++ b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/quizzes/[quizName]/route.ts @@ -6,27 +6,46 @@ export const GET = async ( { params: { courseName, moduleName, quizName }, }: { params: { courseName: string; moduleName: string; quizName: string } } -) => await withErrorHandling(async () => { - const quiz = await fileStorageService.quizzes.getQuiz( - courseName, - moduleName, - quizName - ); - return Response.json(quiz); -}) +) => + await withErrorHandling(async () => { + const quiz = await fileStorageService.quizzes.getQuiz( + courseName, + moduleName, + quizName + ); + return Response.json(quiz); + }); export const PUT = async ( request: Request, { params: { courseName, moduleName, quizName }, }: { params: { courseName: string; moduleName: string; quizName: string } } -) => await withErrorHandling(async () => { - const quiz = await request.json() - await fileStorageService.quizzes.updateQuiz( - courseName, - moduleName, - quizName, - quiz - ); - return Response.json({}); -}) +) => + await withErrorHandling(async () => { + const quiz = await request.json(); + await fileStorageService.quizzes.updateQuiz( + courseName, + moduleName, + quizName, + quiz + ); + return Response.json({}); + }); + +export const POST = async ( + request: Request, + { + params: { courseName, moduleName, quizName }, + }: { params: { courseName: string; moduleName: string; quizName: string } } +) => + await withErrorHandling(async () => { + const quiz = await request.json(); + await fileStorageService.quizzes.updateQuiz( + courseName, + moduleName, + quizName, + quiz + ); + return Response.json({}); + }); diff --git a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx index b0222eb..bcf97a8 100644 --- a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx +++ b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx @@ -13,6 +13,8 @@ import { import { IModuleItem } from "@/models/local/IModuleItem"; import { getDateFromStringOrThrow } from "@/models/local/timeUtils"; import { useState } from "react"; +import Modal from "../../../../components/Modal"; +import NewItemForm from "./NewItemForm"; export default function ExpandableModule({ moduleName, @@ -72,16 +74,21 @@ export default function ExpandableModule({
-
+ + {({ closeModal }) => ( +
+ +
+ +
+ )} +
{moduleItems.map(({ type, item }) => (
{item.name}
))} diff --git a/nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx b/nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx new file mode 100644 index 0000000..f8f370f --- /dev/null +++ b/nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx @@ -0,0 +1,83 @@ +"use client"; +import ButtonSelect from "@/components/ButtonSelect"; +import TextInput from "@/components/form/TextInput"; +import { Spinner } from "@/components/Spinner"; +import { useCreateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks"; +import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; +import { useCreatePageMutation } from "@/hooks/localCourse/pageHooks"; +import { useCreateQuizMutation } from "@/hooks/localCourse/quizHooks"; +import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType"; +import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup"; +import { dateToMarkdownString } from "@/models/local/timeUtils"; +import React, { useState } from "react"; + +export default function NewItemForm({ moduleName }: { moduleName: string }) { + const [type, setType] = useState<"Assignment" | "Quiz" | "Page">( + "Assignment" + ); + const [name, setName] = useState(""); + const [assignmentGroup, setAssignmentGroup] = + useState(); + const { data: settings } = useLocalCourseSettingsQuery(); + const createAssignment = useCreateAssignmentMutation(); + const createPage = useCreatePageMutation(); + const createQuiz = useCreateQuizMutation(); + + const isPending = + createAssignment.isPending || createPage.isPending || createQuiz.isPending; + + return ( +
{ + e.preventDefault(); + if (type === "Assignment") { + createAssignment.mutate({ + assignment: { + name, + description: "", + dueAt: dateToMarkdownString(new Date()), + submissionTypes: [ + AssignmentSubmissionType.ONLINE_TEXT_ENTRY, + AssignmentSubmissionType.ONLINE_UPLOAD, + ], + allowedFileUploadExtensions: ["pdf"], + rubric: [], + }, + moduleName: moduleName, + assignmentName: name, + }); + } else if (type === "Quiz") { + } else if (type === "Page") { + } + }} + > +
+ + options={["Assignment", "Quiz", "Page"]} + getName={(o) => o?.toString() ?? ""} + setSelectedOption={(t) => setType(t ?? "Assignment")} + selectedOption={type} + /> +
+
+ +
+
+ g?.name ?? ""} + setSelectedOption={setAssignmentGroup} + selectedOption={assignmentGroup} + /> +
+ {settings.assignmentGroups.length === 0 && ( +
+ No assignment groups created, create them in the course settings page +
+ )} + + {isPending && } + + ); +} diff --git a/nextjs/src/app/globals.css b/nextjs/src/app/globals.css index 4f80813..deaeccc 100644 --- a/nextjs/src/app/globals.css +++ b/nextjs/src/app/globals.css @@ -83,6 +83,11 @@ button, @apply bg-red-800 hover:bg-red-900 text-red-100; } +.btn-outline { + @apply bg-transparent text-blue-200 border border-blue-500; + @apply hover:border-blue-500 hover:text-blue-100 hover:bg-blue-900; +} + select { @apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm sm:text-sm; @apply focus:outline-none focus:ring-blue-500 focus:border-blue-500; diff --git a/nextjs/src/components/ButtonSelect.tsx b/nextjs/src/components/ButtonSelect.tsx new file mode 100644 index 0000000..3ebe7fd --- /dev/null +++ b/nextjs/src/components/ButtonSelect.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +export default function ButtonSelect({ + options, + getName, + setSelectedOption, + selectedOption, +}: { + options: T[]; + getName: (value: T | undefined) => string; + setSelectedOption: (value: T | undefined) => void; + selectedOption: T | undefined; +}) { + return ( +
+ {options.map((o) => ( + + ))} +
+ ); +} diff --git a/nextjs/src/components/Modal.tsx b/nextjs/src/components/Modal.tsx new file mode 100644 index 0000000..9c8086d --- /dev/null +++ b/nextjs/src/components/Modal.tsx @@ -0,0 +1,44 @@ +"use client"; +import React, { ReactNode, useState } from "react"; + +export default function Modal({ + children, + buttonText, + buttonClass = "", +}: { + children: (props: { closeModal: () => void }) => ReactNode; + buttonText: string; + buttonClass?: string; +}) { + const [isOpen, setIsOpen] = useState(false); + + const openModal = () => setIsOpen(true); + const closeModal = () => setIsOpen(false); + + return ( + <> + + +
+
+ {isOpen && children({ closeModal })} +
+
+ + ); +} diff --git a/nextjs/src/components/form/TextInput.tsx b/nextjs/src/components/form/TextInput.tsx index bfb2494..ad471bd 100644 --- a/nextjs/src/components/form/TextInput.tsx +++ b/nextjs/src/components/form/TextInput.tsx @@ -16,7 +16,7 @@ export default function TextInput({ {label}
setValue(e.target.value)} /> diff --git a/nextjs/src/hooks/localCourse/assignmentHooks.ts b/nextjs/src/hooks/localCourse/assignmentHooks.ts index b4a8cab..d70ca54 100644 --- a/nextjs/src/hooks/localCourse/assignmentHooks.ts +++ b/nextjs/src/hooks/localCourse/assignmentHooks.ts @@ -127,3 +127,46 @@ export const useUpdateAssignmentMutation = () => { }, }); }; + + +export const useCreateAssignmentMutation = () => { + const { courseName } = useCourseContext(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + assignment, + moduleName, + assignmentName, + }: { + assignment: LocalAssignment; + moduleName: string; + assignmentName: string; + }) => { + queryClient.setQueryData( + localCourseKeys.assignment(courseName, moduleName, assignmentName), + assignment + ); + const url = + "/api/courses/" + + encodeURIComponent(courseName) + + "/modules/" + + encodeURIComponent(moduleName) + + "/assignments/" + + encodeURIComponent(assignmentName); + await axiosClient.post(url, assignment); + }, + onSuccess: (_, { moduleName, assignmentName }) => { + queryClient.invalidateQueries({ + queryKey: localCourseKeys.assignment( + courseName, + moduleName, + assignmentName + ), + }); + queryClient.invalidateQueries({ + queryKey: localCourseKeys.assignmentNames(courseName, moduleName), + }); + }, + }); +}; + diff --git a/nextjs/src/hooks/localCourse/pageHooks.ts b/nextjs/src/hooks/localCourse/pageHooks.ts index f74a119..805b2cc 100644 --- a/nextjs/src/hooks/localCourse/pageHooks.ts +++ b/nextjs/src/hooks/localCourse/pageHooks.ts @@ -116,3 +116,41 @@ export const useUpdatePageMutation = () => { }, }); }; + + +export const useCreatePageMutation = () => { + const { courseName } = useCourseContext(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + page, + moduleName, + pageName, + }: { + page: LocalCoursePage; + moduleName: string; + pageName: string; + }) => { + queryClient.setQueryData( + localCourseKeys.page(courseName, moduleName, pageName), + page + ); + const url = + "/api/courses/" + + encodeURIComponent(courseName) + + "/modules/" + + encodeURIComponent(moduleName) + + "/pages/" + + encodeURIComponent(pageName); + await axiosClient.post(url, page); + }, + onSuccess: (_, { moduleName, pageName }) => { + queryClient.invalidateQueries({ + queryKey: localCourseKeys.page(courseName, moduleName, pageName), + }); + queryClient.invalidateQueries({ + queryKey: localCourseKeys.pageNames(courseName, moduleName), + }); + }, + }); +}; diff --git a/nextjs/src/hooks/localCourse/quizHooks.ts b/nextjs/src/hooks/localCourse/quizHooks.ts index 1029e39..73cb88b 100644 --- a/nextjs/src/hooks/localCourse/quizHooks.ts +++ b/nextjs/src/hooks/localCourse/quizHooks.ts @@ -105,3 +105,41 @@ export const useUpdateQuizMutation = () => { }, }); }; + + +export const useCreateQuizMutation = () => { + const { courseName } = useCourseContext(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + quiz, + moduleName, + quizName, + }: { + quiz: LocalQuiz; + moduleName: string; + quizName: string; + }) => { + queryClient.setQueryData( + localCourseKeys.quiz(courseName, moduleName, quizName), + quiz + ); + const url = + "/api/courses/" + + encodeURIComponent(courseName) + + "/modules/" + + encodeURIComponent(moduleName) + + "/quizzes/" + + encodeURIComponent(quizName); + await axiosClient.post(url, quiz); + }, + onSuccess: (_, { moduleName, quizName }) => { + queryClient.invalidateQueries({ + queryKey: localCourseKeys.quiz(courseName, moduleName, quizName), + }); + queryClient.invalidateQueries({ + queryKey: localCourseKeys.quizNames(courseName, moduleName), + }); + }, + }); +}; diff --git a/nextjs/src/services/fileStorage/fileStorageService.ts b/nextjs/src/services/fileStorage/fileStorageService.ts index 700ca6a..384884f 100644 --- a/nextjs/src/services/fileStorage/fileStorageService.ts +++ b/nextjs/src/services/fileStorage/fileStorageService.ts @@ -147,12 +147,21 @@ export const fileStorageService = { ); return localAssignmentMarkdown.parseMarkdown(rawFile); }, - async updateAssignment( + async updateOrCreateAssignment( courseName: string, moduleName: string, assignmentName: string, assignment: LocalAssignment ) { + const folder = path.join( + basePath, + courseName, + moduleName, + "assignments", + ); + await fs.mkdir(folder, { recursive: true }); + + const filePath = path.join( basePath, courseName, @@ -201,6 +210,13 @@ export const fileStorageService = { quizName: string, quiz: LocalQuiz ) { + const folder = path.join( + basePath, + courseName, + moduleName, + "quizzes", + ); + await fs.mkdir(folder, { recursive: true }); const filePath = path.join( basePath, courseName, @@ -248,6 +264,14 @@ export const fileStorageService = { pageName: string, page: LocalCoursePage ) { + const folder = path.join( + basePath, + courseName, + moduleName, + "pages", + ); + await fs.mkdir(folder, { recursive: true }); + const filePath = path.join( basePath, courseName,