From 14003d52c97ef097fcdb5d6a8affdccd8c33fa39 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Sat, 2 Nov 2024 13:17:36 -0600 Subject: [PATCH] prevent lectures from dropping on each other --- .../course/[courseName]/calendar/day/Day.tsx | 38 +----- .../[courseName]/calendar/day/DayTitle.tsx | 53 ++++++++ .../context/DraggingContextProvider.tsx | 115 ++++++++++++++---- .../[courseName]/context/draggingContext.tsx | 4 +- .../lecture/[lectureDay]/EditLecture.tsx | 2 +- .../app/todaysLectures/OneCourseLectures.tsx | 5 +- nextjs/src/hooks/localCourse/lectureHooks.ts | 13 +- nextjs/src/models/local/lectureUtils.ts | 9 ++ .../fileStorage/lectureFileStorageService.ts | 40 +++++- 9 files changed, 212 insertions(+), 67 deletions(-) create mode 100644 nextjs/src/app/course/[courseName]/calendar/day/DayTitle.tsx create mode 100644 nextjs/src/models/local/lectureUtils.ts diff --git a/nextjs/src/app/course/[courseName]/calendar/day/Day.tsx b/nextjs/src/app/course/[courseName]/calendar/day/Day.tsx index 577a35a..5c23b46 100644 --- a/nextjs/src/app/course/[courseName]/calendar/day/Day.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/day/Day.tsx @@ -1,20 +1,14 @@ "use client"; import { - dateToMarkdownString, getDateFromStringOrThrow, getDateOnlyMarkdownString, } from "@/models/local/timeUtils"; import { useDraggingContext } from "../../context/draggingContext"; -import { useCourseContext } from "../../context/courseContext"; -import Link from "next/link"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; import { getDayOfWeek } from "@/models/local/localCourse"; -import { getLectureUrl } from "@/services/urlUtils"; import { ItemInDay } from "./ItemInDay"; import { useTodaysItems } from "./useTodaysItems"; -import Modal from "@/components/Modal"; -import NewItemForm from "../../modules/NewItemForm"; -import { useLecturesByWeekQuery } from "@/hooks/localCourse/lectureHooks"; +import { DayTitle } from "./DayTitle"; export default function Day({ day, month }: { day: string; month: number }) { const dayAsDate = getDateFromStringOrThrow( @@ -130,33 +124,3 @@ export default function Day({ day, month }: { day: string; month: number }) { ); } - -function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) { - const { courseName } = useCourseContext(); - const { data: weeks } = useLecturesByWeekQuery(); - const todaysLecture = weeks - .flatMap((w) => w.lectures) - .find((l) => l.date == getDateOnlyMarkdownString(dayAsDate)); - return ( -
- - {dayAsDate.getDate()} {todaysLecture?.name} - - - {({ closeModal }) => ( -
- -
- -
- )} -
-
- ); -} diff --git a/nextjs/src/app/course/[courseName]/calendar/day/DayTitle.tsx b/nextjs/src/app/course/[courseName]/calendar/day/DayTitle.tsx new file mode 100644 index 0000000..0211136 --- /dev/null +++ b/nextjs/src/app/course/[courseName]/calendar/day/DayTitle.tsx @@ -0,0 +1,53 @@ +import Modal from "@/components/Modal"; +import { useLecturesByWeekQuery } from "@/hooks/localCourse/lectureHooks"; +import { getLectureUrl } from "@/services/urlUtils"; +import Link from "next/link"; +import { useCourseContext } from "../../context/courseContext"; +import NewItemForm from "../../modules/NewItemForm"; +import { DraggableItem } from "../../context/draggingContext"; +import { useDragStyleContext } from "../../context/dragStyleContext"; +import { getLectureForDay } from "@/models/local/lectureUtils"; + +export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) { + const { courseName } = useCourseContext(); + const { data: weeks } = useLecturesByWeekQuery(); + const { setIsDragging } = useDragStyleContext(); + const todaysLecture = getLectureForDay(weeks, dayAsDate); + return ( +
+ { + if (todaysLecture) { + const draggableItem: DraggableItem = { + type: "lecture", + item: { ...todaysLecture, dueAt: todaysLecture.date }, + sourceModuleName: undefined, + }; + e.dataTransfer.setData( + "draggableItem", + JSON.stringify(draggableItem) + ); + setIsDragging(true); + } + }} + > + {dayAsDate.getDate()} {todaysLecture?.name} + + + {({ closeModal }) => ( +
+ +
+ +
+ )} +
+
+ ); +} diff --git a/nextjs/src/app/course/[courseName]/context/DraggingContextProvider.tsx b/nextjs/src/app/course/[courseName]/context/DraggingContextProvider.tsx index 6fb47d3..fa85135 100644 --- a/nextjs/src/app/course/[courseName]/context/DraggingContextProvider.tsx +++ b/nextjs/src/app/course/[courseName]/context/DraggingContextProvider.tsx @@ -7,12 +7,19 @@ import { LocalQuiz } from "@/models/local/quiz/localQuiz"; import { getDateFromStringOrThrow, dateToMarkdownString, + getDateOnlyMarkdownString, } from "@/models/local/timeUtils"; import { LocalAssignment } from "@/models/local/assignment/localAssignment"; import { useUpdateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks"; import { useUpdatePageMutation } from "@/hooks/localCourse/pageHooks"; import { LocalCoursePage } from "@/models/local/page/localCoursePage"; import { useDragStyleContext } from "./dragStyleContext"; +import { Lecture } from "@/models/local/lecture"; +import { + useLecturesByWeekQuery, + useLectureUpdateMutation, +} from "@/hooks/localCourse/lectureHooks"; +import { getLectureForDay } from "@/models/local/lectureUtils"; export default function DraggingContextProvider({ children, @@ -21,9 +28,11 @@ export default function DraggingContextProvider({ }) { const { setIsDragging } = useDragStyleContext(); const updateQuizMutation = useUpdateQuizMutation(); + const updateLectureMutation = useLectureUpdateMutation(); const updateAssignmentMutation = useUpdateAssignmentMutation(); const updatePageMutation = useUpdatePageMutation(); const { data: settings } = useLocalCourseSettingsQuery(); + const { data: weeks } = useLecturesByWeekQuery(); useEffect(() => { const handleDrop = () => { @@ -55,40 +64,63 @@ export default function DraggingContextProvider({ updateAssignment(); } else if (itemBeingDragged.type === "page") { updatePage(); + } else if (itemBeingDragged.type === "lecture") { + // const lecture = itemBeingDragged.item as Lecture & { dueAt: string }; + console.log("cannot drop lecture on module, only on days"); } } setIsDragging(false); function updateQuiz() { const quiz = itemBeingDragged.item as LocalQuiz; - - updateQuizMutation.mutate({ - item: quiz, - itemName: quiz.name, - moduleName: dropModuleName, - previousModuleName: itemBeingDragged.sourceModuleName, - previousItemName: quiz.name, - }); + if (itemBeingDragged.sourceModuleName) { + updateQuizMutation.mutate({ + item: quiz, + itemName: quiz.name, + moduleName: dropModuleName, + previousModuleName: itemBeingDragged.sourceModuleName, + previousItemName: quiz.name, + }); + } else { + console.error( + `error dropping quiz, sourceModuleName is undefined `, + quiz + ); + } } function updateAssignment() { const assignment = itemBeingDragged.item as LocalAssignment; - updateAssignmentMutation.mutate({ - item: assignment, - previousModuleName: itemBeingDragged.sourceModuleName, - moduleName: dropModuleName, - itemName: assignment.name, - previousItemName: assignment.name, - }); + if (itemBeingDragged.sourceModuleName) { + updateAssignmentMutation.mutate({ + item: assignment, + previousModuleName: itemBeingDragged.sourceModuleName, + moduleName: dropModuleName, + itemName: assignment.name, + previousItemName: assignment.name, + }); + } else { + console.error( + `error dropping assignment, sourceModuleName is undefined `, + assignment + ); + } } function updatePage() { const page = itemBeingDragged.item as LocalCoursePage; - updatePageMutation.mutate({ - item: page, - moduleName: dropModuleName, - itemName: page.name, - previousItemName: page.name, - previousModuleName: itemBeingDragged.sourceModuleName, - }); + if (itemBeingDragged.sourceModuleName) { + updatePageMutation.mutate({ + item: page, + moduleName: dropModuleName, + itemName: page.name, + previousItemName: page.name, + previousModuleName: itemBeingDragged.sourceModuleName, + }); + } else { + console.error( + `error dropping page, sourceModuleName is undefined `, + page + ); + } } }, [ @@ -113,6 +145,8 @@ export default function DraggingContextProvider({ updateAssignment(dayAsDate); } else if (itemBeingDragged.type === "page") { updatePage(dayAsDate); + } else if (itemBeingDragged.type === "lecture") { + updateLecture(dayAsDate); } } setIsDragging(false); @@ -124,8 +158,32 @@ export default function DraggingContextProvider({ dayAsDate.setSeconds(0); return dayAsDate; } + function updateLecture(dayAsDate: Date) { + const { dueAt, ...lecture } = itemBeingDragged.item as Lecture & { + dueAt: string; + }; + console.log("dropped lecture on day"); + const existingLecture = getLectureForDay(weeks, dayAsDate); + if (existingLecture) { + console.log("attempting to drop on existing lecture"); + } else { + updateLectureMutation.mutate({ + previousDay: lecture.date, + lecture: { + ...lecture, + date: getDateOnlyMarkdownString(dayAsDate), + }, + }); + } + } function updateQuiz(dayAsDate: Date) { const previousQuiz = itemBeingDragged.item as LocalQuiz; + if (!itemBeingDragged.sourceModuleName) { + console.error( + "error dropping quiz on day, sourceModuleName is undefined" + ); + return; + } const quiz: LocalQuiz = { ...previousQuiz, @@ -146,6 +204,12 @@ export default function DraggingContextProvider({ } function updatePage(dayAsDate: Date) { const previousPage = itemBeingDragged.item as LocalCoursePage; + if (!itemBeingDragged.sourceModuleName) { + console.error( + "error dropping page on day, sourceModuleName is undefined" + ); + return; + } const page: LocalCoursePage = { ...previousPage, dueAt: dateToMarkdownString(dayAsDate), @@ -159,6 +223,12 @@ export default function DraggingContextProvider({ }); } function updateAssignment(dayAsDate: Date) { + if (!itemBeingDragged.sourceModuleName) { + console.error( + "error dropping assignment on day, sourceModuleName is undefined" + ); + return; + } const previousAssignment = itemBeingDragged.item as LocalAssignment; const assignment: LocalAssignment = { ...previousAssignment, @@ -199,6 +269,7 @@ export default function DraggingContextProvider({ ); } + function getNewLockDate( originalDueDate: string, originalLockDate: string | undefined, diff --git a/nextjs/src/app/course/[courseName]/context/draggingContext.tsx b/nextjs/src/app/course/[courseName]/context/draggingContext.tsx index 4b208f3..6fa64b8 100644 --- a/nextjs/src/app/course/[courseName]/context/draggingContext.tsx +++ b/nextjs/src/app/course/[courseName]/context/draggingContext.tsx @@ -4,8 +4,8 @@ import { createContext, useContext, DragEvent } from "react"; export interface DraggableItem { item: IModuleItem; - sourceModuleName: string; - type: "quiz" | "assignment" | "page"; + sourceModuleName: string | undefined; // undefined for lectures + type: "quiz" | "assignment" | "page" | "lecture"; } export interface DraggingContextInterface { diff --git a/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx b/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx index d010b88..d9add61 100644 --- a/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx +++ b/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx @@ -38,7 +38,7 @@ Date: ${lectureDay} const parsed = parseLecture(text); if (!lecture || lectureToString(parsed) !== lectureToString(lecture)) { console.log("updating lecture"); - updateLecture.mutate(parsed); + updateLecture.mutate({ lecture: parsed }); } setError(""); } catch (e: any) { diff --git a/nextjs/src/app/todaysLectures/OneCourseLectures.tsx b/nextjs/src/app/todaysLectures/OneCourseLectures.tsx index 2e8ca6e..2718a4d 100644 --- a/nextjs/src/app/todaysLectures/OneCourseLectures.tsx +++ b/nextjs/src/app/todaysLectures/OneCourseLectures.tsx @@ -5,6 +5,7 @@ import { getDateOnlyMarkdownString } from "@/models/local/timeUtils"; import { getLecturePreviewUrl } from "@/services/urlUtils"; import Link from "next/link"; import { useCourseContext } from "../course/[courseName]/context/courseContext"; +import { getLectureForDay } from "@/models/local/lectureUtils"; export default function OneCourseLectures() { const { courseName } = useCourseContext(); @@ -12,9 +13,7 @@ export default function OneCourseLectures() { const dayAsDate = new Date(); const dayAsString = getDateOnlyMarkdownString(dayAsDate); - const todaysLecture = weeks - .flatMap((w) => w.lectures) - .find((l) => l.date == dayAsString); + const todaysLecture = getLectureForDay(weeks, dayAsDate); if (!todaysLecture) return <>; return ( { const { data: settings } = useLocalCourseSettingsQuery(); const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (lecture: Lecture) => { + mutationFn: async ({ + lecture, + previousDay, + }: { + lecture: Lecture; + previousDay?: string; + }) => { await updateLecture(courseName, settings, lecture); + + if (previousDay && previousDay !== lecture.date) { + await deleteLecture(courseName, settings, previousDay); + } }, onSuccess: () => { queryClient.invalidateQueries({ diff --git a/nextjs/src/models/local/lectureUtils.ts b/nextjs/src/models/local/lectureUtils.ts new file mode 100644 index 0000000..15bdbcd --- /dev/null +++ b/nextjs/src/models/local/lectureUtils.ts @@ -0,0 +1,9 @@ +import { Lecture } from "./lecture"; +import { getDateOnlyMarkdownString } from "./timeUtils"; + +export function getLectureForDay(weeks: { weekName: string; lectures: Lecture[]; }[], dayAsDate: Date) { + return weeks + .flatMap((w) => w.lectures) + .find((l) => l.date == getDateOnlyMarkdownString(dayAsDate)); +} + diff --git a/nextjs/src/services/fileStorage/lectureFileStorageService.ts b/nextjs/src/services/fileStorage/lectureFileStorageService.ts index b679215..884c680 100644 --- a/nextjs/src/services/fileStorage/lectureFileStorageService.ts +++ b/nextjs/src/services/fileStorage/lectureFileStorageService.ts @@ -57,7 +57,10 @@ export async function updateLecture( "lecture start date in update lecture" ); - const weekFolderName = getLectureWeekName(courseSettings.startDate, lecture.date); + const weekFolderName = getLectureWeekName( + courseSettings.startDate, + lecture.date + ); const weekPath = path.join(courseLectureRoot, weekFolderName); if (!(await directoryExists(weekPath))) { await fs.mkdir(weekPath, { recursive: true }); @@ -71,6 +74,41 @@ export async function updateLecture( await fs.writeFile(lecturePath, lectureContents); } +export async function deleteLecture( + courseName: string, + courseSettings: LocalCourseSettings, + dayAsString: string +) { + console.log("deleting lecture", courseName, dayAsString); + const lectureDate = getDateFromStringOrThrow( + dayAsString, + "lecture start date in update lecture" + ); + + const weekFolderName = getLectureWeekName( + courseSettings.startDate, + dayAsString + ); + + const courseLectureRoot = path.join(basePath, courseName, lectureFolderName); + const weekPath = path.join(courseLectureRoot, weekFolderName); + const lecturePath = path.join( + weekPath, + `${lectureDate.getDay()}-${getDayOfWeek(lectureDate)}.md` + ); + try { + await fs.access(lecturePath); // throws error if no file + await fs.unlink(lecturePath); + console.log(`File deleted: ${lecturePath}`); + } catch (error: any) { + if (error?.code === "ENOENT") { + console.log(`Cannot delete lecture, file does not exist: ${lecturePath}`); + } else { + throw error; + } + } +} + const directoryExists = async (path: string): Promise => { try { const stat = await fs.stat(path);