From e1f14015925a9a9e0fc368d14e582e286fe8f532 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Mon, 9 Sep 2024 17:01:21 -0600 Subject: [PATCH] improved performance on calendar view --- .../[courseName]/calendar/CourseCalendar.tsx | 9 +- .../app/course/[courseName]/calendar/Day.tsx | 160 ++++++++++++++++-- .../context/CalendarItemsContextProvider.tsx | 155 +++++++++++++++++ .../context/calendarItemsContext.ts | 22 +++ ...vnasCouresHooks.ts => canvasCourseKeys.ts} | 0 .../src/hooks/localCourse/assignmentHooks.ts | 51 ++++-- .../hooks/localCourse/localCoursesHooks.ts | 157 ++++++++++++++--- nextjs/src/hooks/localCourse/pageHooks.ts | 18 +- nextjs/src/hooks/localCourse/quizHooks.ts | 17 +- nextjs/src/models/local/timeUtils.ts | 5 + 10 files changed, 522 insertions(+), 72 deletions(-) create mode 100644 nextjs/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx create mode 100644 nextjs/src/app/course/[courseName]/context/calendarItemsContext.ts rename nextjs/src/hooks/{cavnasCouresHooks.ts => canvasCourseKeys.ts} (100%) diff --git a/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx b/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx index c8d8d15..c11581d 100644 --- a/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx @@ -4,6 +4,7 @@ import { getMonthsBetweenDates } from "./calendarMonthUtils"; import { CalendarMonth } from "./CalendarMonth"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; import { useMemo } from "react"; +import CalendarItemsContextProvider from "../context/CalendarItemsContextProvider"; export default function CourseCalendar() { const { data: settings } = useLocalCourseSettingsQuery(); @@ -32,9 +33,11 @@ export default function CourseCalendar() { bg-slate-950 " > - {months.map((month) => ( - - ))} + + {months.map((month) => ( + + ))} + ); } diff --git a/nextjs/src/app/course/[courseName]/calendar/Day.tsx b/nextjs/src/app/course/[courseName]/calendar/Day.tsx index ec5af6d..e297aa4 100644 --- a/nextjs/src/app/course/[courseName]/calendar/Day.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/Day.tsx @@ -1,19 +1,27 @@ "use client"; -import { useModuleNamesQuery } from "@/hooks/localCourse/localCoursesHooks"; -import DayItemsInModule from "./DayItemsInModule"; -import { getDateFromStringOrThrow } from "@/models/local/timeUtils"; +import { + getDateFromStringOrThrow, + getDateOnlyMarkdownString, +} from "@/models/local/timeUtils"; import { useDraggingContext } from "../context/draggingContext"; +import { useCalendarItemsContext } from "../context/calendarItemsContext"; +import { useCourseContext } from "../context/courseContext"; +import Link from "next/link"; export default function Day({ day, month }: { day: string; month: number }) { - const { data: moduleNames } = useModuleNamesQuery(); - const dayAsDate = getDateFromStringOrThrow( day, "calculating same month in day" ); const isInSameMonth = dayAsDate.getMonth() + 1 != month; const backgroundClass = isInSameMonth ? "" : "bg-slate-900"; + + const itemsContext = useCalendarItemsContext(); const { itemDrop } = useDraggingContext(); + const { courseName } = useCourseContext(); + + const dateKey = getDateOnlyMarkdownString(dayAsDate); + const todaysModules = itemsContext[dateKey]; return (
e.preventDefault()} > {dayAsDate.getDate()} - {moduleNames.map((moduleName) => ( - - ))} + {todaysModules && + Object.keys(todaysModules).flatMap((moduleName) => + todaysModules[moduleName].assignments.map((a) => ( +
  • { + e.dataTransfer.setData( + "draggableItem", + JSON.stringify({ + type: "assignment", + item: a, + sourceModuleName: moduleName, + }) + ); + }} + > + + {a.name} + +
  • + )) + )} + {todaysModules && + Object.keys(todaysModules).flatMap((moduleName) => + todaysModules[moduleName].quizzes.map((q) => ( +
  • { + e.dataTransfer.setData( + "draggableItem", + JSON.stringify({ + type: "quiz", + item: q, + sourceModuleName: moduleName, + }) + ); + }} + > + + {q.name} + +
  • + )) + )} + {todaysModules && + Object.keys(todaysModules).flatMap((moduleName) => + todaysModules[moduleName].pages.map((p) => ( +
  • { + e.dataTransfer.setData( + "draggableItem", + JSON.stringify({ + type: "page", + item: p, + sourceModuleName: moduleName, + }) + ); + }} + > + + {p.name} + +
  • + )) + )}
    ); } + +// export default function Day({ day, month }: { day: string; month: number }) { +// const { data: moduleNames } = useModuleNamesQuery(); + +// const dayAsDate = getDateFromStringOrThrow( +// day, +// "calculating same month in day" +// ); +// const isInSameMonth = dayAsDate.getMonth() + 1 != month; +// const backgroundClass = isInSameMonth ? "" : "bg-slate-900"; +// const { itemDrop } = useDraggingContext(); + +// return ( +//
    { +// itemDrop(e, day); +// }} +// onDragOver={(e) => e.preventDefault()} +// > +// {dayAsDate.getDate()} +// {moduleNames.map((moduleName) => ( +// +// ))} +//
    +// ); +// } diff --git a/nextjs/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx b/nextjs/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx new file mode 100644 index 0000000..962d281 --- /dev/null +++ b/nextjs/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx @@ -0,0 +1,155 @@ +import { ReactNode } from "react"; +import { useCourseContext } from "./courseContext"; +import { + useAllCourseDataQuery, + useModuleDataQuery, +} from "@/hooks/localCourse/localCoursesHooks"; +import { + CalendarItemsContext, + CalendarItemsInterface, +} from "./calendarItemsContext"; +import { + dateToMarkdownString, + getDateFromStringOrThrow, + getDateOnlyMarkdownString, +} from "@/models/local/timeUtils"; +import { LocalAssignment } from "@/models/local/assignment/localAssignment"; +import { LocalQuiz } from "@/models/local/quiz/localQuiz"; +import { LocalCoursePage } from "@/models/local/page/localCoursePage"; + +export default function CalendarItemsContextProvider({ + children, +}: { + children: ReactNode; +}) { + const { assignmentsAndModules, quizzesAndModules, pagesAndModules } = + useAllCourseDataQuery(); + + const assignmentsByModuleByDate = assignmentsAndModules.reduce( + (previous, { assignment, moduleName }) => { + const dueDay = getDateOnlyMarkdownString( + getDateFromStringOrThrow( + assignment.dueAt, + "due at for assignment in items context" + ) + ); + const previousModules = previous[dueDay] ?? {}; + const previousModule = previousModules[moduleName] ?? { + assignments: [], + }; + + const updatedModule = { + ...previousModule, + assignments: [...previousModule.assignments, assignment], + }; + + return { + ...previous, + [dueDay]: { + ...previousModules, + [moduleName]: updatedModule, + }, + }; + }, + {} as CalendarItemsInterface + ); + + const quizzesByModuleByDate = quizzesAndModules.reduce( + (previous, { quiz, moduleName }) => { + const dueDay = getDateOnlyMarkdownString( + getDateFromStringOrThrow(quiz.dueAt, "due at for quiz in items context") + ); + const previousModules = previous[dueDay] ?? {}; + const previousModule = previousModules[moduleName] ?? { + quizzes: [], + }; + + const updatedModule = { + ...previousModule, + quizzes: [...previousModule.quizzes, quiz], + }; + + return { + ...previous, + [dueDay]: { + ...previousModules, + [moduleName]: updatedModule, + }, + }; + }, + {} as CalendarItemsInterface + ); + + const pagesByModuleByDate = pagesAndModules.reduce( + (previous, { page, moduleName }) => { + const dueDay = getDateOnlyMarkdownString( + getDateFromStringOrThrow(page.dueAt, "due at for quiz in items context") + ); + const previousModules = previous[dueDay] ?? {}; + const previousModule = previousModules[moduleName] ?? { + pages: [], + }; + + const updatedModule = { + ...previousModule, + pages: [...previousModule.pages, page], + }; + + return { + ...previous, + [dueDay]: { + ...previousModules, + [moduleName]: updatedModule, + }, + }; + }, + {} as CalendarItemsInterface + ); + + const allDays = [ + ...new Set([ + ...Object.keys(assignmentsByModuleByDate), + ...Object.keys(quizzesByModuleByDate), + ...Object.keys(pagesByModuleByDate), + ]), + ]; + + const allItemsByModuleByDate = allDays.reduce((prev, day) => { + const assignmentModulesInDay = assignmentsByModuleByDate[day] ?? {}; + const quizModulesInDay = quizzesByModuleByDate[day] ?? {}; + const pageModulesInDay = pagesByModuleByDate[day] ?? {}; + + const allModules = [ + ...new Set([ + ...Object.keys(assignmentModulesInDay), + ...Object.keys(quizModulesInDay), + ...Object.keys(pageModulesInDay), + ]), + ]; + + const modulesInDate = allModules.reduce((prev, moduleName) => { + return { + ...prev, + [moduleName]: { + assignments: assignmentModulesInDay[moduleName] + ? assignmentModulesInDay[moduleName].assignments + : [], + quizzes: quizModulesInDay[moduleName] + ? quizModulesInDay[moduleName].quizzes + : [], + pages: pageModulesInDay[moduleName] + ? pageModulesInDay[moduleName].pages + : [], + }, + }; + }, {}); + + return { ...prev, [day]: modulesInDate }; + }, {} as CalendarItemsInterface); + + return ( + + {children} + + ); +} diff --git a/nextjs/src/app/course/[courseName]/context/calendarItemsContext.ts b/nextjs/src/app/course/[courseName]/context/calendarItemsContext.ts new file mode 100644 index 0000000..168a948 --- /dev/null +++ b/nextjs/src/app/course/[courseName]/context/calendarItemsContext.ts @@ -0,0 +1,22 @@ +import { LocalAssignment } from "@/models/local/assignment/localAssignment"; +import { LocalCoursePage } from "@/models/local/page/localCoursePage"; +import { LocalQuiz } from "@/models/local/quiz/localQuiz"; +import { createContext, useContext } from "react"; + +export interface CalendarItemsInterface { + [ + key: string // representing a date + ]: { + [moduleName: string]: { + assignments: LocalAssignment[]; + quizzes: LocalQuiz[]; + pages: LocalCoursePage[]; + }; + }; +} + +export const CalendarItemsContext = createContext({}); + +export function useCalendarItemsContext() { + return useContext(CalendarItemsContext); +} diff --git a/nextjs/src/hooks/cavnasCouresHooks.ts b/nextjs/src/hooks/canvasCourseKeys.ts similarity index 100% rename from nextjs/src/hooks/cavnasCouresHooks.ts rename to nextjs/src/hooks/canvasCourseKeys.ts diff --git a/nextjs/src/hooks/localCourse/assignmentHooks.ts b/nextjs/src/hooks/localCourse/assignmentHooks.ts index 1d80e5f..1687285 100644 --- a/nextjs/src/hooks/localCourse/assignmentHooks.ts +++ b/nextjs/src/hooks/localCourse/assignmentHooks.ts @@ -1,28 +1,40 @@ -"use client" +"use client"; import axios from "axios"; import { localCourseKeys } from "./localCourseKeys"; import { LocalAssignment } from "@/models/local/assignment/localAssignment"; -import { useSuspenseQuery, useSuspenseQueries, useQueryClient, useMutation } from "@tanstack/react-query"; +import { + useSuspenseQuery, + useSuspenseQueries, + useQueryClient, + useMutation, +} from "@tanstack/react-query"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; +export const getAssignmentNamesQueryConfig = ( + courseName: string, + moduleName: string +) => ({ + queryKey: localCourseKeys.assignmentNames(courseName, moduleName), + queryFn: async (): Promise => { + const url = + "/api/courses/" + + encodeURIComponent(courseName) + + "/modules/" + + encodeURIComponent(moduleName) + + "/assignments"; + const response = await axios.get(url); + return response.data; + }, +}); + export const useAssignmentNamesQuery = (moduleName: string) => { const { courseName } = useCourseContext(); - return useSuspenseQuery({ - queryKey: localCourseKeys.assignmentNames(courseName, moduleName), - queryFn: async (): Promise => { - const url = - "/api/courses/" + - encodeURIComponent(courseName) + - "/modules/" + - encodeURIComponent(moduleName) + - "/assignments"; - const response = await axios.get(url); - return response.data; - }, - }); + return useSuspenseQuery( + getAssignmentNamesQueryConfig(courseName, moduleName) + ); }; -const getAssignmentQueryConfig = ( +export const getAssignmentQueryConfig = ( courseName: string, moduleName: string, assignmentName: string @@ -46,6 +58,7 @@ const getAssignmentQueryConfig = ( }, }; }; + export const useAssignmentQuery = ( moduleName: string, assignmentName: string @@ -101,7 +114,11 @@ export const useUpdateAssignmentMutation = () => { }, onSuccess: (_, { moduleName, assignmentName }) => { queryClient.invalidateQueries({ - queryKey: localCourseKeys.assignment(courseName, moduleName, assignmentName), + queryKey: localCourseKeys.assignment( + courseName, + moduleName, + assignmentName + ), }); queryClient.invalidateQueries({ queryKey: localCourseKeys.assignmentNames(courseName, moduleName), diff --git a/nextjs/src/hooks/localCourse/localCoursesHooks.ts b/nextjs/src/hooks/localCourse/localCoursesHooks.ts index 9dbc070..8641711 100644 --- a/nextjs/src/hooks/localCourse/localCoursesHooks.ts +++ b/nextjs/src/hooks/localCourse/localCoursesHooks.ts @@ -1,14 +1,20 @@ "use client"; import { LocalCourseSettings } from "@/models/local/localCourse"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { + useQueries, + useSuspenseQueries, + useSuspenseQuery, +} from "@tanstack/react-query"; import axios from "axios"; import { localCourseKeys } from "./localCourseKeys"; import { + getAssignmentNamesQueryConfig, + getAssignmentQueryConfig, useAssignmentNamesQuery, useAssignmentsQueries, } from "./assignmentHooks"; -import { usePageNamesQuery, usePagesQueries } from "./pageHooks"; -import { useQuizNamesQuery, useQuizzesQueries } from "./quizHooks"; +import { getPageNamesQueryConfig, getPageQueryConfig, usePageNamesQuery, usePagesQueries } from "./pageHooks"; +import { getQuizNamesQueryConfig, getQuizQueryConfig, useQuizNamesQuery, useQuizzesQueries } from "./quizHooks"; import { useMemo } from "react"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; @@ -48,33 +54,128 @@ export const useModuleNamesQuery = () => { // dangerous? really slowed down page... // maybe it only slowed down with react query devtools... -// export const useModuleDataQuery = (moduleName: string) => { -// console.log("running"); -// const { data: assignmentNames } = useAssignmentNamesQuery(moduleName); -// const { data: quizNames } = useQuizNamesQuery(moduleName); -// const { data: pageNames } = usePageNamesQuery(moduleName); +export const useModuleDataQuery = (moduleName: string) => { + console.log("running"); + const { data: assignmentNames } = useAssignmentNamesQuery(moduleName); + const { data: quizNames } = useQuizNamesQuery(moduleName); + const { data: pageNames } = usePageNamesQuery(moduleName); -// const { data: assignments } = useAssignmentsQueries( -// moduleName, -// assignmentNames -// ); -// const { data: quizzes } = useQuizzesQueries(moduleName, quizNames); -// const { data: pages } = usePagesQueries(moduleName, pageNames); + const { data: assignments } = useAssignmentsQueries( + moduleName, + assignmentNames + ); + const { data: quizzes } = useQuizzesQueries(moduleName, quizNames); + const { data: pages } = usePagesQueries(moduleName, pageNames); -// return { -// assignments, -// quizzes, -// pages, -// }; -// // return useMemo( -// // () => ({ -// // assignments, -// // quizzes, -// // pages, -// // }), -// // [assignments, pages, quizzes] -// // ); -// }; + return { + assignments, + quizzes, + pages, + }; + // return useMemo( + // () => ({ + // assignments, + // quizzes, + // pages, + // }), + // [assignments, pages, quizzes] + // ); +}; + +export const useAllCourseDataQuery = () => { + const { courseName } = useCourseContext(); + const { data: moduleNames } = useModuleNamesQuery(); + + const { data: assignmentNamesAndModules } = useSuspenseQueries({ + queries: moduleNames.map((moduleName) => + getAssignmentNamesQueryConfig(courseName, moduleName) + ), + combine: (results) => ({ + data: results.flatMap((r, i) => + r.data.map((assignmentName) => ({ + moduleName: moduleNames[i], + assignmentName, + })) + ), + pending: results.some((r) => r.isPending), + }), + }); + + const { data: assignmentsAndModules } = useSuspenseQueries({ + queries: assignmentNamesAndModules.map( + ({ moduleName, assignmentName }, i) => + getAssignmentQueryConfig(courseName, moduleName, assignmentName) + ), + combine: (results) => ({ + data: results.flatMap((r, i) => ({ + moduleName: assignmentNamesAndModules[i].moduleName, + assignment: r.data, + })), + pending: results.some((r) => r.isPending), + }), + }); + + const {data: quizNamesAndModules } = useSuspenseQueries({ + queries: moduleNames.map((moduleName) => + getQuizNamesQueryConfig(courseName, moduleName) + ), + combine: (results) => ({ + data: results.flatMap((r, i) => + r.data.map((quizName) => ({ + moduleName: moduleNames[i], + quizName: quizName, + })) + ), + pending: results.some((r) => r.isPending), + }), + }); + + const { data: quizzesAndModules } = useSuspenseQueries({ + queries: quizNamesAndModules.map( + ({ moduleName, quizName }, i) => + getQuizQueryConfig(courseName, moduleName, quizName) + ), + combine: (results) => ({ + data: results.flatMap((r, i) => ({ + moduleName: quizNamesAndModules[i].moduleName, + quiz: r.data, + })), + pending: results.some((r) => r.isPending), + }), + }); + + const {data: pageNamesAndModules } = useSuspenseQueries({ + queries: moduleNames.map((moduleName) => + getPageNamesQueryConfig(courseName, moduleName) + ), + combine: (results) => ({ + data: results.flatMap((r, i) => + r.data.map((pageName) => ({ + moduleName: moduleNames[i], + pageName, + })) + ), + pending: results.some((r) => r.isPending), + }), + }); + + const { data: pagesAndModules } = useSuspenseQueries({ + queries: pageNamesAndModules.map( + ({ moduleName, pageName }, i) => + getPageQueryConfig(courseName, moduleName, pageName) + ), + combine: (results) => ({ + data: results.flatMap((r, i) => ({ + moduleName: pageNamesAndModules[i].moduleName, + page: r.data, + })), + pending: results.some((r) => r.isPending), + }), + }); + + + return { assignmentsAndModules, quizzesAndModules, pagesAndModules }; +}; // export const useUpdateCourseMutation = (courseName: string) => { // const queryClient = useQueryClient(); diff --git a/nextjs/src/hooks/localCourse/pageHooks.ts b/nextjs/src/hooks/localCourse/pageHooks.ts index bdbf00a..4c65e73 100644 --- a/nextjs/src/hooks/localCourse/pageHooks.ts +++ b/nextjs/src/hooks/localCourse/pageHooks.ts @@ -10,9 +10,11 @@ import axios from "axios"; import { localCourseKeys } from "./localCourseKeys"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; -export const usePageNamesQuery = (moduleName: string) => { - const { courseName } = useCourseContext(); - return useSuspenseQuery({ +export function getPageNamesQueryConfig( + courseName: string, + moduleName: string +) { + return { queryKey: localCourseKeys.pageNames(courseName, moduleName), queryFn: async (): Promise => { const url = @@ -24,8 +26,14 @@ export const usePageNamesQuery = (moduleName: string) => { const response = await axios.get(url); return response.data; }, - }); + }; +} + +export const usePageNamesQuery = (moduleName: string) => { + const { courseName } = useCourseContext(); + return useSuspenseQuery(getPageNamesQueryConfig(courseName, moduleName)); }; + export const usePageQuery = (moduleName: string, pageName: string) => { const { courseName } = useCourseContext(); return useSuspenseQuery(getPageQueryConfig(courseName, moduleName, pageName)); @@ -44,7 +52,7 @@ export const usePagesQueries = (moduleName: string, pageNames: string[]) => { }); }; -function getPageQueryConfig( +export function getPageQueryConfig( courseName: string, moduleName: string, pageName: string diff --git a/nextjs/src/hooks/localCourse/quizHooks.ts b/nextjs/src/hooks/localCourse/quizHooks.ts index f306f43..e4e6ff2 100644 --- a/nextjs/src/hooks/localCourse/quizHooks.ts +++ b/nextjs/src/hooks/localCourse/quizHooks.ts @@ -10,13 +10,12 @@ import axios from "axios"; import { localCourseKeys } from "./localCourseKeys"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; -export const useQuizNamesQuery = (moduleName: string) => { - const { courseName } = useCourseContext(); - return useSuspenseQuery({ + +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) + @@ -24,7 +23,11 @@ export const useQuizNamesQuery = (moduleName: string) => { const response = await axios.get(url); return response.data; }, - }); + }; +} +export const useQuizNamesQuery = (moduleName: string) => { + const { courseName } = useCourseContext(); + return useSuspenseQuery(getQuizNamesQueryConfig(courseName, moduleName)); }; export const useQuizQuery = (moduleName: string, quizName: string) => { @@ -45,7 +48,7 @@ export const useQuizzesQueries = (moduleName: string, quizNames: string[]) => { }); }; -function getQuizQueryConfig( +export function getQuizQueryConfig( courseName: string, moduleName: string, quizName: string diff --git a/nextjs/src/models/local/timeUtils.ts b/nextjs/src/models/local/timeUtils.ts index c32b936..70e5c2d 100644 --- a/nextjs/src/models/local/timeUtils.ts +++ b/nextjs/src/models/local/timeUtils.ts @@ -91,3 +91,8 @@ export const dateToMarkdownString = (date: Date) => { return `${stringMonth}/${stringDay}/${stringYear} ${stringHours}:${stringMinutes}:${stringSeconds}`; }; + + +export const getDateOnlyMarkdownString = (date: Date) => { + return dateToMarkdownString(date).split(" ")[0] +} \ No newline at end of file