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 1940f19..f14c5e2 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,11 +28,13 @@ export const PUT = async ( } ) => await withErrorHandling(async () => { - const { - assignment, - previousModuleName, - }: { assignment: LocalAssignment; previousModuleName?: string } = - await request.json(); + const { assignment, previousModuleName, previousAssignmentName } = + (await request.json()) as { + assignment: LocalAssignment; + previousModuleName?: string; + previousAssignmentName?: string; + }; + await fileStorageService.assignments.updateOrCreateAssignment({ courseName, moduleName, @@ -40,9 +42,17 @@ export const PUT = async ( assignment, }); - if(previousModuleName !== moduleName) - { - fileStorageService.assignments. + if ( + previousModuleName && + previousAssignmentName && + (assignment.name !== previousAssignmentName || + moduleName !== previousModuleName) + ) { + fileStorageService.assignments.delete({ + courseName, + moduleName: previousModuleName, + assignmentName: previousAssignmentName, + }); } return Response.json({}); 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 25da7fc..4d3e5e9 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 @@ -1,3 +1,4 @@ +import { LocalQuiz } from "@/models/local/quiz/localQuiz"; import { fileStorageService } from "@/services/fileStorage/fileStorageService"; import { withErrorHandling } from "@/services/withErrorHandling"; @@ -23,13 +24,31 @@ export const PUT = async ( }: { params: { courseName: string; moduleName: string; quizName: string } } ) => await withErrorHandling(async () => { - const quiz = await request.json(); + const { quiz, previousModuleName, previousQuizName } = + (await request.json()) as { + quiz: LocalQuiz; + previousModuleName?: string; + previousQuizName?: string; + }; await fileStorageService.quizzes.updateQuiz( courseName, moduleName, quizName, quiz ); + + if ( + previousModuleName && + previousQuizName && + (quiz.name !== previousQuizName || + moduleName !== previousModuleName) + ) { + fileStorageService.quizzes.delete({ + courseName, + moduleName: previousModuleName, + quizName: previousQuizName, + }); + } return Response.json({}); }); diff --git a/nextjs/src/app/course/[courseName]/calendar/Day.tsx b/nextjs/src/app/course/[courseName]/calendar/Day.tsx index de0b059..a8b295a 100644 --- a/nextjs/src/app/course/[courseName]/calendar/Day.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/Day.tsx @@ -3,7 +3,7 @@ import { getDateFromStringOrThrow, getDateOnlyMarkdownString, } from "@/models/local/timeUtils"; -import { useDraggingContext } from "../context/draggingContext"; +import { DraggableItem, useDraggingContext } from "../context/draggingContext"; import { useCalendarItemsContext } from "../context/calendarItemsContext"; import { useCourseContext } from "../context/courseContext"; import Link from "next/link"; @@ -40,7 +40,7 @@ export default function Day({ day, month }: { day: string; month: number }) { const meetingClasses = classOnThisDay ? " bg-slate-900 " : " "; const monthClass = isInSameMonth ? isToday - ? " border border-blue-700 border-[3px] shadow-[0_0px_10px_0px] shadow-blue-500/50 " + ? " border border-blue-700 shadow-[0_0px_10px_0px] shadow-blue-500/50 " : " border border-slate-700 " : " "; @@ -50,7 +50,7 @@ export default function Day({ day, month }: { day: string; month: number }) { onDrop={(e) => itemDropOnDay(e, day)} onDragOver={(e) => e.preventDefault()} > - +
{todaysAssignments.map(({ assignment, moduleName, status }) => ( @@ -192,8 +192,8 @@ function DraggableListItem({ href={getModuleItemUrl(courseName, moduleName, type, item.name)} shallow={true} className={ - " border rounded-sm px-1 mx-1 break-all mb-1 " + - " bg-slate-800 " + + " border rounded-sm px-1 mx-1 break-words mb-1 " + + " bg-slate-800 " + " block " + (status === "localOnly" && " text-slate-500 border-slate-600 ") + (status === "unPublished" && " border-rose-900 ") + @@ -202,14 +202,12 @@ function DraggableListItem({ role="button" draggable="true" onDragStart={(e) => { - e.dataTransfer.setData( - "draggableItem", - JSON.stringify({ - type, - item, - sourceModuleName: moduleName, - }) - ); + const draggableItem: DraggableItem = { + type, + item, + sourceModuleName: moduleName, + }; + e.dataTransfer.setData("draggableItem", JSON.stringify(draggableItem)); dragStart(); }} > diff --git a/nextjs/src/app/course/[courseName]/context/DraggingContextProvider.tsx b/nextjs/src/app/course/[courseName]/context/DraggingContextProvider.tsx index 6c6dfc8..8a67e7e 100644 --- a/nextjs/src/app/course/[courseName]/context/DraggingContextProvider.tsx +++ b/nextjs/src/app/course/[courseName]/context/DraggingContextProvider.tsx @@ -1,6 +1,6 @@ "use client"; -import { ReactNode, useCallback, DragEvent, useState } from "react"; -import { DraggingContext } from "./draggingContext"; +import { ReactNode, useCallback, DragEvent, useState, useEffect } from "react"; +import { DraggableItem, DraggingContext } from "./draggingContext"; import { useUpdateQuizMutation } from "@/hooks/localCourse/quizHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; import { LocalQuiz } from "@/models/local/quiz/localQuiz"; @@ -24,12 +24,29 @@ export default function DraggingContextProvider({ const { data: settings } = useLocalCourseSettingsQuery(); const [isDragging, setIsDragging] = useState(false); + useEffect(() => { + const handleDrop = () => { + console.log("drop on window"); + setIsDragging(false); + }; + const preventDefault = (e: globalThis.DragEvent) => e.preventDefault(); + if (typeof window !== "undefined") { + window.addEventListener("drop", handleDrop); + window.addEventListener("dragover", preventDefault); + } + return () => { + window.removeEventListener("drop", handleDrop); + window.addEventListener("dragover", preventDefault); + }; + }, []); + const dragStart = useCallback(() => setIsDragging(true), []); const itemDropOnModule = useCallback( - (e: DragEvent, moduleName: string) => { + (e: DragEvent, dropModuleName: string) => { + console.log("dropping on module"); const rawData = e.dataTransfer.getData("draggableItem"); - const itemBeingDragged = JSON.parse(rawData); + const itemBeingDragged: DraggableItem = JSON.parse(rawData); if (itemBeingDragged) { if (itemBeingDragged.type === "quiz") { @@ -42,17 +59,45 @@ export default function DraggingContextProvider({ } setIsDragging(false); - function updateQuiz() {} - function updateAssignment() {} - function updatePage() {} + function updateQuiz() { + const quiz = itemBeingDragged.item as LocalQuiz; + + updateQuizMutation.mutate({ + quiz: quiz, + quizName: quiz.name, + moduleName: dropModuleName, + previousModuleName: itemBeingDragged.sourceModuleName, + previousQuizName: quiz.name, + }); + } + function updateAssignment() { + const assignment = itemBeingDragged.item as LocalAssignment; + updateAssignmentMutation.mutate({ + assignment, + previousModuleName: itemBeingDragged.sourceModuleName, + moduleName: dropModuleName, + assignmentName: assignment.name, + previousAssignmentName: assignment.name, + }); + } + function updatePage() { + const page = itemBeingDragged.item as LocalCoursePage; + updatePageMutation.mutate({ + page, + moduleName: dropModuleName, + pageName: page.name, + previousPageName: page.name, + previousModuleName: itemBeingDragged.sourceModuleName, + }); + } }, - [] + [updateAssignmentMutation, updatePageMutation, updateQuizMutation] ); const itemDropOnDay = useCallback( (e: DragEvent, day: string) => { const rawData = e.dataTransfer.getData("draggableItem"); - const itemBeingDragged = JSON.parse(rawData); + const itemBeingDragged: DraggableItem = JSON.parse(rawData); if (itemBeingDragged) { const dayAsDate = getDateWithDefaultDueTime(); @@ -85,6 +130,8 @@ export default function DraggingContextProvider({ quiz: quiz, quizName: quiz.name, moduleName: itemBeingDragged.sourceModuleName, + previousModuleName: itemBeingDragged.sourceModuleName, + previousQuizName: quiz.name, }); } function updatePage(dayAsDate: Date) { @@ -97,6 +144,8 @@ export default function DraggingContextProvider({ page, moduleName: itemBeingDragged.sourceModuleName, pageName: page.name, + previousPageName: page.name, + previousModuleName: itemBeingDragged.sourceModuleName, }); } function updateAssignment(dayAsDate: Date) { @@ -115,8 +164,10 @@ export default function DraggingContextProvider({ }; updateAssignmentMutation.mutate({ assignment, + previousModuleName: itemBeingDragged.sourceModuleName, moduleName: itemBeingDragged.sourceModuleName, assignmentName: assignment.name, + previousAssignmentName: assignment.name, }); } }, diff --git a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx index cd45a9d..2933527 100644 --- a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx +++ b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx @@ -13,7 +13,6 @@ import { } from "@/hooks/localCourse/quizHooks"; import { IModuleItem } from "@/models/local/IModuleItem"; import { - dateToMarkdownString, getDateFromString, getDateFromStringOrThrow, getDateOnlyMarkdownString, @@ -24,6 +23,8 @@ import NewItemForm from "./NewItemForm"; import { ModuleCanvasStatus } from "./ModuleCanvasStatus"; import ClientOnly from "@/components/ClientOnly"; import ExpandIcon from "../../../../components/icons/ExpandIcon"; +import { useDraggingContext } from "../context/draggingContext"; +import DropTargetStyling from "../calendar/DropTargetStyling"; export default function ExpandableModule({ moduleName, @@ -33,6 +34,7 @@ export default function ExpandableModule({ const { data: assignmentNames } = useAssignmentNamesQuery(moduleName); const { data: quizNames } = useQuizNamesQuery(moduleName); const { data: pageNames } = usePageNamesQuery(moduleName); + const { itemDropOnModule } = useDraggingContext(); const { data: assignments } = useAssignmentsQueries( moduleName, @@ -74,56 +76,63 @@ export default function ExpandableModule({ const expandRef = useRef(null); return ( -
-
setExpanded((e) => !e)} - > -
{moduleName}
-
- - - - -
-
-
- - {({ closeModal }) => ( -
- -
- +
itemDropOnModule(e, moduleName)} + onDragOver={(e) => e.preventDefault()} + > + +
+
setExpanded((e) => !e)} + > +
{moduleName}
+
+ + + +
- )} - -
+
+
+ + {({ closeModal }) => ( +
+ +
+ +
+ )} +
+
+ {moduleItems.map(({ type, item }) => { + const date = getDateFromString(item.dueAt); - {moduleItems.map(({ type, item }) => { - const date = getDateFromString(item.dueAt); - - return ( - -
- {date && getDateOnlyMarkdownString(date)} -
-
{item.name}
-
- ); - })} + return ( + +
+ {date && getDateOnlyMarkdownString(date)} +
+
{item.name}
+
+ ); + })} +
+
-
+
); } diff --git a/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/EditAssignment.tsx b/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/EditAssignment.tsx index e59f3cc..6af2900 100644 --- a/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/EditAssignment.tsx +++ b/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/EditAssignment.tsx @@ -56,6 +56,8 @@ export default function EditAssignment({ assignment: updatedAssignment, moduleName, assignmentName, + previousModuleName: moduleName, + previousAssignmentName: assignment.name, }); } setError(""); diff --git a/nextjs/src/hooks/localCourse/assignmentHooks.ts b/nextjs/src/hooks/localCourse/assignmentHooks.ts index 4bf6e8d..8cbf1d4 100644 --- a/nextjs/src/hooks/localCourse/assignmentHooks.ts +++ b/nextjs/src/hooks/localCourse/assignmentHooks.ts @@ -95,13 +95,36 @@ export const useUpdateAssignmentMutation = () => { assignment, moduleName, previousModuleName, + previousAssignmentName, assignmentName, }: { assignment: LocalAssignment; moduleName: string; previousModuleName: string; + previousAssignmentName: string; assignmentName: string; }) => { + if ( + previousAssignmentName && + previousModuleName && + (previousAssignmentName !== assignment.name || + previousModuleName !== moduleName) + ) { + queryClient.removeQueries({ + queryKey: localCourseKeys.assignment( + courseName, + previousModuleName, + previousAssignmentName + ), + }); + queryClient.removeQueries({ + queryKey: localCourseKeys.assignmentNames( + courseName, + previousModuleName + ), + }); + } + queryClient.setQueryData( localCourseKeys.assignment(courseName, moduleName, assignmentName), assignment @@ -113,7 +136,11 @@ export const useUpdateAssignmentMutation = () => { encodeURIComponent(moduleName) + "/assignments/" + encodeURIComponent(assignmentName); - await axiosClient.put(url, { assignment, previousModuleName }); + await axiosClient.put(url, { + assignment, + previousModuleName, + previousAssignmentName, + }); }, onSuccess: (_, { moduleName, assignmentName }) => { queryClient.invalidateQueries({ diff --git a/nextjs/src/hooks/localCourse/pageHooks.ts b/nextjs/src/hooks/localCourse/pageHooks.ts index 3368180..5877748 100644 --- a/nextjs/src/hooks/localCourse/pageHooks.ts +++ b/nextjs/src/hooks/localCourse/pageHooks.ts @@ -87,11 +87,31 @@ export const useUpdatePageMutation = () => { page, moduleName, pageName, + previousModuleName, + previousPageName, }: { page: LocalCoursePage; moduleName: string; pageName: string; + previousModuleName: string; + previousPageName: string; }) => { + if ( + previousPageName && + previousModuleName && + (previousPageName !== page.name || previousModuleName !== moduleName) + ) { + queryClient.removeQueries({ + queryKey: localCourseKeys.page( + courseName, + previousModuleName, + previousPageName + ), + }); + queryClient.removeQueries({ + queryKey: localCourseKeys.pageNames(courseName, moduleName), + }); + } queryClient.setQueryData( localCourseKeys.page(courseName, moduleName, pageName), page @@ -116,7 +136,6 @@ export const useUpdatePageMutation = () => { }); }; - export const useCreatePageMutation = () => { const { courseName } = useCourseContext(); const queryClient = useQueryClient(); diff --git a/nextjs/src/hooks/localCourse/quizHooks.ts b/nextjs/src/hooks/localCourse/quizHooks.ts index 73cb88b..2bfc68d 100644 --- a/nextjs/src/hooks/localCourse/quizHooks.ts +++ b/nextjs/src/hooks/localCourse/quizHooks.ts @@ -10,12 +10,15 @@ import { localCourseKeys } from "./localCourseKeys"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; import { axiosClient } from "@/services/axiosUtils"; - -export function getQuizNamesQueryConfig(courseName: string, moduleName: string) { +export function getQuizNamesQueryConfig( + courseName: string, + moduleName: string +) { return { queryKey: localCourseKeys.quizNames(courseName, moduleName), queryFn: async (): Promise => { - const url = "/api/courses/" + + const url = + "/api/courses/" + encodeURIComponent(courseName) + "/modules/" + encodeURIComponent(moduleName) + @@ -77,11 +80,31 @@ export const useUpdateQuizMutation = () => { quiz, moduleName, quizName, + previousModuleName, + previousQuizName, }: { quiz: LocalQuiz; moduleName: string; quizName: string; + previousModuleName: string; + previousQuizName: string; }) => { + if ( + previousQuizName && + previousModuleName && + (previousQuizName !== quiz.name || previousModuleName !== moduleName) + ) { + queryClient.removeQueries({ + queryKey: localCourseKeys.quiz( + courseName, + previousModuleName, + previousQuizName + ), + }); + queryClient.removeQueries({ + queryKey: localCourseKeys.quizNames(courseName, previousModuleName), + }); + } queryClient.setQueryData( localCourseKeys.quiz(courseName, moduleName, quizName), quiz @@ -93,7 +116,15 @@ export const useUpdateQuizMutation = () => { encodeURIComponent(moduleName) + "/quizzes/" + encodeURIComponent(quizName); - await axiosClient.put(url, quiz); + await axiosClient.put(url, { + quiz, + previousModuleName, + previousQuizName, + }); + + // queryClient.fetchQuery( + // getQuizNamesQueryConfig(courseName, previousModuleName) + // ); }, onSuccess: (_, { moduleName, quizName }) => { queryClient.invalidateQueries({ @@ -106,7 +137,6 @@ export const useUpdateQuizMutation = () => { }); }; - export const useCreateQuizMutation = () => { const { courseName } = useCourseContext(); const queryClient = useQueryClient(); diff --git a/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts b/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts index b7fd661..9f3662d 100644 --- a/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts +++ b/nextjs/src/models/local/quiz/utils/quizMarkdownUtils.ts @@ -1,4 +1,7 @@ -import { verifyDateOrThrow, verifyDateStringOrUndefined } from "../../timeUtils"; +import { + verifyDateOrThrow, + verifyDateStringOrUndefined, +} from "../../timeUtils"; import { LocalQuiz } from "../localQuiz"; import { quizQuestionMarkdownUtils } from "./quizQuestionMarkdownUtils"; @@ -76,7 +79,6 @@ const getQuizWithOnlySettings = (settings: string): LocalQuiz => { const rawDueAt = extractLabelValue(settings, "DueAt"); const dueAt = verifyDateOrThrow(rawDueAt, "DueAt"); - const rawLockAt = extractLabelValue(settings, "LockAt"); const lockAt = verifyDateStringOrUndefined(rawLockAt); @@ -104,6 +106,16 @@ const getQuizWithOnlySettings = (settings: string): LocalQuiz => { export const quizMarkdownUtils = { toMarkdown(quiz: LocalQuiz): string { + if (!quiz) { + throw Error(`quiz was undefined, cannot parse markdown`); + } + if ( + typeof quiz.questions === "undefined" || + typeof quiz.oneQuestionAtATime === "undefined" + ) { + console.log("quiz is probably not a quiz", quiz); + throw Error(`quiz ${quiz.name} is probably not a quiz`); + } const questionMarkdownArray = quiz.questions.map((q) => quizQuestionMarkdownUtils.toMarkdown(q) ); diff --git a/nextjs/src/services/fileStorage/assignmentsFileStorageService.ts b/nextjs/src/services/fileStorage/assignmentsFileStorageService.ts index ab5b16e..8316594 100644 --- a/nextjs/src/services/fileStorage/assignmentsFileStorageService.ts +++ b/nextjs/src/services/fileStorage/assignmentsFileStorageService.ts @@ -65,4 +65,24 @@ export const assignmentsFileStorageService = { console.log(`Saving assignment ${filePath}`); await fs.writeFile(filePath, assignmentMarkdown); }, + async delete({ + courseName, + moduleName, + assignmentName, + }: { + courseName: string; + moduleName: string; + assignmentName: string; + }) { + + const filePath = path.join( + basePath, + courseName, + moduleName, + "assignments", + assignmentName + ".md" + ); + console.log("removing assignment", filePath); + await fs.unlink(filePath) + } }; diff --git a/nextjs/src/services/fileStorage/quizFileStorageService.ts b/nextjs/src/services/fileStorage/quizFileStorageService.ts index 9b0d0b0..6d7db39 100644 --- a/nextjs/src/services/fileStorage/quizFileStorageService.ts +++ b/nextjs/src/services/fileStorage/quizFileStorageService.ts @@ -55,4 +55,23 @@ export const quizFileStorageService = { console.log(`Saving quiz ${filePath}`); await fs.writeFile(filePath, quizMarkdown); }, + async delete({ + courseName, + moduleName, + quizName, + }: { + courseName: string; + moduleName: string; + quizName: string; + }) { + const filePath = path.join( + basePath, + courseName, + moduleName, + "quizzes", + quizName + ".md" + ); + console.log("removing quiz", filePath); + await fs.unlink(filePath) + } };