pushing state down with contexts

This commit is contained in:
2024-09-02 11:57:38 -06:00
parent 130035cc48
commit 895271743f
20 changed files with 517 additions and 439 deletions

View File

@@ -2,8 +2,7 @@
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
export default function CourseSettings() {
export default function CourseSettings({ courseName }: { courseName: string }) { const { data: settings } = useLocalCourseSettingsQuery();
const { data: settings } = useLocalCourseSettingsQuery(courseName);
return <div>{settings.name}</div>; return <div>{settings.name}</div>;
} }

View File

@@ -52,7 +52,7 @@ function CalendarWeek({
week, week,
monthNumber, monthNumber,
}: { }: {
week: Date[]; week: string[]; //date strings
monthNumber: number; monthNumber: number;
}) { }) {
return ( return (

View File

@@ -1,14 +1,12 @@
"use client"; "use client";
import { getDateFromStringOrThrow } from "@/models/local/timeUtils"; import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
import { useCourseContext } from "../context/courseContext";
import { getMonthsBetweenDates } from "./calendarMonthUtils"; import { getMonthsBetweenDates } from "./calendarMonthUtils";
import { CalendarMonth } from "./CalendarMonth"; import { CalendarMonth } from "./CalendarMonth";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useMemo } from "react"; import { useMemo } from "react";
export default function CourseCalendar() { export default function CourseCalendar() {
const { courseName } = useCourseContext(); const { data: settings } = useLocalCourseSettingsQuery();
const { data: settings } = useLocalCourseSettingsQuery(courseName);
const startDateTime = useMemo( const startDateTime = useMemo(
() => getDateFromStringOrThrow(settings.startDate, "course start date"), () => getDateFromStringOrThrow(settings.startDate, "course start date"),

View File

@@ -1,12 +1,16 @@
"use client"; "use client";
import { useCourseContext } from "../context/courseContext";
import { useModuleNamesQuery } from "@/hooks/localCourse/localCoursesHooks"; import { useModuleNamesQuery } from "@/hooks/localCourse/localCoursesHooks";
import DayItemsInModule from "./DayItemsInModule"; import DayItemsInModule from "./DayItemsInModule";
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
import { useDraggingContext } from "../context/DraggingContext";
export default function Day({ day, month }: { day: Date; month: number }) { export default function Day({ day, month }: { day: string; month: number }) {
const { courseName, itemDrop } = useCourseContext(); const { data: moduleNames } = useModuleNamesQuery();
const { data: moduleNames } = useModuleNamesQuery(courseName);
const isInSameMonth = day.getMonth() + 1 != month; const dayAsDate = getDateFromStringOrThrow(day, "calculating same month in day")
const isInSameMonth =
dayAsDate.getMonth() + 1 !=
month;
const backgroundClass = isInSameMonth ? "" : "bg-slate-900"; const backgroundClass = isInSameMonth ? "" : "bg-slate-900";
return ( return (
@@ -15,16 +19,23 @@ export default function Day({ day, month }: { day: Date; month: number }) {
"border border-slate-600 rounded-lg p-2 pb-4 m-1 " + backgroundClass "border border-slate-600 rounded-lg p-2 pb-4 m-1 " + backgroundClass
} }
> >
{day.getDate()} {dayAsDate.getDate()}
{moduleNames.map((moduleName) => ( {moduleNames.map((moduleName) => (
<div <ModuleInDay
key={"" + day + month + moduleName} key={"" + day + month + moduleName}
onDrop={() => itemDrop(day)} moduleName={moduleName}
onDragOver={(e) => e.preventDefault()} day={day}
> />
<DayItemsInModule day={day} moduleName={moduleName} />
</div>
))} ))}
</div> </div>
); );
} }
function ModuleInDay({ moduleName, day }: { moduleName: string; day: string }) {
const { itemDrop } = useDraggingContext();
return (
<div onDrop={() => itemDrop(day)} onDragOver={(e) => e.preventDefault()}>
<DayItemsInModule day={day} moduleName={moduleName} />
</div>
);
}

View File

@@ -7,20 +7,20 @@ import Link from "next/link";
import { LocalAssignment } from "@/models/local/assignmnet/localAssignment"; import { LocalAssignment } from "@/models/local/assignmnet/localAssignment";
import { LocalQuiz } from "@/models/local/quiz/localQuiz"; import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { LocalCoursePage } from "@/models/local/page/localCoursePage"; import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { useDraggingContext } from "../context/DraggingContext";
export default function DayItemsInModule({ export default function DayItemsInModule({
day, day,
moduleName, moduleName,
}: { }: {
day: Date; day: string;
moduleName: string; moduleName: string;
}) { }) {
const { courseName, endItemDrag, startItemDrag } = useCourseContext(); const { courseName } = useCourseContext();
const { endItemDrag, startItemDrag } = useDraggingContext();
const { assignments, quizzes, pages } = useModuleDataQuery( const { assignments, quizzes, pages } = useModuleDataQuery(
courseName,
moduleName moduleName
); );
const todaysAssignments = useMemo( const todaysAssignments = useMemo(
() => () =>
assignments.filter((a) => { assignments.filter((a) => {
@@ -28,10 +28,14 @@ export default function DayItemsInModule({
a.dueAt, a.dueAt,
"due at for assignment in day" "due at for assignment in day"
); );
const dayAsDate = getDateFromStringOrThrow(
day,
"in assignment in DayItemsInModule"
);
return ( return (
dueDate.getFullYear() === day.getFullYear() && dueDate.getFullYear() === dayAsDate.getFullYear() &&
dueDate.getMonth() === day.getMonth() && dueDate.getMonth() === dayAsDate.getMonth() &&
dueDate.getDate() === day.getDate() dueDate.getDate() === dayAsDate.getDate()
); );
}), }),
[assignments, day] [assignments, day]
@@ -43,10 +47,14 @@ export default function DayItemsInModule({
q.dueAt, q.dueAt,
"due at for quiz in day" "due at for quiz in day"
); );
const dayAsDate = getDateFromStringOrThrow(
day,
"in quizzes in DayItemsInModule"
);
return ( return (
dueDate.getFullYear() === day.getFullYear() && dueDate.getFullYear() === dayAsDate.getFullYear() &&
dueDate.getMonth() === day.getMonth() && dueDate.getMonth() === dayAsDate.getMonth() &&
dueDate.getDate() === day.getDate() dueDate.getDate() === dayAsDate.getDate()
); );
}), }),
[day, quizzes] [day, quizzes]
@@ -58,10 +66,14 @@ export default function DayItemsInModule({
p.dueAt, p.dueAt,
"due at for page in day" "due at for page in day"
); );
const dayAsDate = getDateFromStringOrThrow(
day,
"in pages in DayItemsInModule"
);
return ( return (
dueDate.getFullYear() === day.getFullYear() && dueDate.getFullYear() === dayAsDate.getFullYear() &&
dueDate.getMonth() === day.getMonth() && dueDate.getMonth() === dayAsDate.getMonth() &&
dueDate.getDate() === day.getDate() dueDate.getDate() === dayAsDate.getDate()
); );
}), }),
[day, pages] [day, pages]
@@ -128,6 +140,7 @@ export default function DayItemsInModule({
role="button" role="button"
draggable="true" draggable="true"
onDragStart={starPageDrag(p)} onDragStart={starPageDrag(p)}
onDragEnd={endItemDrag}
> >
{p.name} {p.name}
</li> </li>

View File

@@ -1,9 +1,15 @@
"use client" "use client";
import {
dateToMarkdownString,
getDateFromStringOrThrow,
} from "@/models/local/timeUtils";
export interface CalendarMonthModel { export interface CalendarMonthModel {
year: number; year: number;
month: number; month: number;
weeks: number[][]; weeks: number[][];
daysByWeek: (Date)[][]; daysByWeek: string[][]; //iso date is memo-izable
} }
function weeksInMonth(year: number, month: number): number { function weeksInMonth(year: number, month: number): number {
@@ -27,17 +33,25 @@ function createCalendarMonth(year: number, month: number): CalendarMonthModel {
const daysByWeek = Array.from({ length: weeksNumber }).map((_, weekIndex) => const daysByWeek = Array.from({ length: weeksNumber }).map((_, weekIndex) =>
Array.from({ length: 7 }).map((_, dayIndex) => { Array.from({ length: 7 }).map((_, dayIndex) => {
if (weekIndex === 0 && dayIndex < firstDayOfMonth) { if (weekIndex === 0 && dayIndex < firstDayOfMonth) {
return new Date(year, month - 1, dayIndex - firstDayOfMonth + 1); return dateToMarkdownString(
new Date(year, month - 1, dayIndex - firstDayOfMonth + 1)
);
} else if (currentDay <= daysInMonth) { } else if (currentDay <= daysInMonth) {
return new Date(year, month - 1, currentDay++); return dateToMarkdownString(new Date(year, month - 1, currentDay++));
} else { } else {
currentDay++; currentDay++;
return new Date(year, month, currentDay - daysInMonth - 1); return dateToMarkdownString(
new Date(year, month, currentDay - daysInMonth - 1)
);
} }
}) })
); );
const weeks = daysByWeek.map((week) => week.map((day) => day.getDate())); const weeks = daysByWeek.map((week) =>
week.map((day) =>
getDateFromStringOrThrow(day, "calculating weeks").getDate()
)
);
return { year, month, weeks, daysByWeek }; return { year, month, weeks, daysByWeek };
} }

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { ReactNode, useCallback, useState } from "react"; import { ReactNode, useCallback, useState } from "react";
import { CourseContext, DraggableItem } from "./courseContext"; import { CourseContext } from "./courseContext";
import { DraggableItem } from "./DraggingContext";
import { LocalQuiz } from "@/models/local/quiz/localQuiz"; import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import { import {
dateToMarkdownString, dateToMarkdownString,
@@ -16,60 +17,10 @@ export default function CourseContextProvider({
children: ReactNode; children: ReactNode;
localCourseName: string; localCourseName: string;
}) { }) {
const updateQuizMutation = useUpdateQuizMutation(localCourseName);
const { data: settings } = useLocalCourseSettingsQuery(localCourseName);
const [itemBeingDragged, setItemBeingDragged] = useState<
DraggableItem | undefined
>();
const itemDrop = useCallback(
(day: Date | undefined) => {
if (itemBeingDragged && day) {
day.setHours(settings.defaultDueTime.hour);
day.setHours(settings.defaultDueTime.minute);
if (itemBeingDragged.type === "quiz") {
const previousQuiz = itemBeingDragged.item as LocalQuiz;
const quiz: LocalQuiz = {
...previousQuiz,
dueAt: dateToMarkdownString(day),
lockAt:
previousQuiz.lockAt &&
(getDateFromStringOrThrow(previousQuiz.lockAt, "lockAt date") >
day
? previousQuiz.lockAt
: dateToMarkdownString(day)),
};
updateQuizMutation.mutate({
quiz: quiz,
quizName: quiz.name,
moduleName: itemBeingDragged.sourceModuleName,
});
}
}
setItemBeingDragged(undefined);
},
[
itemBeingDragged,
settings.defaultDueTime.hour,
settings.defaultDueTime.minute,
updateQuizMutation,
]
);
const startItemDrag = useCallback((d: DraggableItem) => {
setItemBeingDragged(d);
}, []);
const endItemDrag = useCallback(() => {
setItemBeingDragged(undefined);
}, []);
return ( return (
<CourseContext.Provider <CourseContext.Provider
value={{ value={{
courseName: localCourseName, courseName: localCourseName,
startItemDrag: startItemDrag,
endItemDrag: endItemDrag,
itemDrop,
}} }}
> >
{children} {children}

View File

@@ -0,0 +1,25 @@
"use client";
import { IModuleItem } from "@/models/local/IModuleItem";
import { createContext, useContext } from "react";
export interface DraggableItem {
item: IModuleItem;
sourceModuleName: string;
type: "quiz" | "assignment" | "page";
}
export interface DraggingContextInterface {
startItemDrag: (dragging: DraggableItem) => void;
endItemDrag: () => void;
itemDrop: (droppedOnDay?: string) => void;
}
const defaultDraggingValue: DraggingContextInterface = {
startItemDrag: () => { },
endItemDrag: () => { },
itemDrop: () => { },
};
export const DraggingContext = createContext<DraggingContextInterface>(defaultDraggingValue);
export function useDraggingContext() {
return useContext(DraggingContext);
}

View File

@@ -0,0 +1,82 @@
"use client";
import { ReactNode, useCallback, useState } from "react";
import { DraggableItem, DraggingContext } from "./DraggingContext";
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import {
dateToMarkdownString,
getDateFromStringOrThrow,
} from "@/models/local/timeUtils";
import { useUpdateQuizMutation } from "@/hooks/localCourse/quizHooks";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
export default function DraggingContextProvider({
children,
}: {
children: ReactNode;
localCourseName: string;
}) {
const updateQuizMutation = useUpdateQuizMutation();
const { data: settings } = useLocalCourseSettingsQuery();
const [itemBeingDragged, setItemBeingDragged] = useState<
DraggableItem | undefined
>();
const itemDrop = useCallback(
(day: string | undefined) => {
if (itemBeingDragged && day) {
const dayAsDate = getDateFromStringOrThrow(day, "in drop callback");
dayAsDate.setHours(settings.defaultDueTime.hour);
dayAsDate.setHours(settings.defaultDueTime.minute);
if (itemBeingDragged.type === "quiz") {
console.log("dropping quiz");
const previousQuiz = itemBeingDragged.item as LocalQuiz;
const quiz: LocalQuiz = {
...previousQuiz,
dueAt: dateToMarkdownString(dayAsDate),
lockAt:
previousQuiz.lockAt &&
(getDateFromStringOrThrow(previousQuiz.lockAt, "lockAt date") >
dayAsDate
? previousQuiz.lockAt
: dateToMarkdownString(dayAsDate)),
};
updateQuizMutation.mutate({
quiz: quiz,
quizName: quiz.name,
moduleName: itemBeingDragged.sourceModuleName,
});
} else if (itemBeingDragged.type === "assignment") {
console.log("dropped assignment");
} else if (itemBeingDragged.type === "page") {
console.log("dropped page");
}
}
setItemBeingDragged(undefined);
},
[
itemBeingDragged,
settings.defaultDueTime.hour,
settings.defaultDueTime.minute,
updateQuizMutation,
]
);
const startItemDrag = useCallback((d: DraggableItem) => {
setItemBeingDragged(d);
}, []);
const endItemDrag = useCallback(() => {
setItemBeingDragged(undefined);
}, []);
return (
<DraggingContext.Provider
value={{
startItemDrag: startItemDrag,
endItemDrag: endItemDrag,
itemDrop,
}}
>
{children}
</DraggingContext.Provider>
);
}

View File

@@ -1,25 +1,12 @@
"use client"; "use client";
import { IModuleItem } from "@/models/local/IModuleItem";
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
export interface DraggableItem {
item: IModuleItem;
sourceModuleName: string;
type: "quiz" | "assignment" | "page";
}
export interface CourseContextInterface { export interface CourseContextInterface {
courseName: string; courseName: string;
startItemDrag: (dragging: DraggableItem) => void;
endItemDrag: () => void;
itemDrop: (droppedOnDay?: Date) => void;
} }
const defaultValue: CourseContextInterface = { const defaultValue: CourseContextInterface = {
startItemDrag: () => { }, courseName: "",
endItemDrag: () => { },
itemDrop: () => { },
courseName: ""
}; };
export const CourseContext = export const CourseContext =
@@ -28,3 +15,4 @@ export const CourseContext =
export function useCourseContext() { export function useCourseContext() {
return useContext(CourseContext); return useContext(CourseContext);
} }

View File

@@ -9,9 +9,7 @@ export default function ExpandableModule({
}: { }: {
moduleName: string; moduleName: string;
}) { }) {
const { courseName } = useCourseContext();
const { assignments, quizzes, pages } = useModuleDataQuery( const { assignments, quizzes, pages } = useModuleDataQuery(
courseName,
moduleName moduleName
); );

View File

@@ -4,8 +4,7 @@ import { useCourseContext } from "../context/courseContext";
import ExpandableModule from "./ExpandableModule"; import ExpandableModule from "./ExpandableModule";
export default function ModuleList() { export default function ModuleList() {
const { courseName } = useCourseContext(); const { data: moduleNames } = useModuleNamesQuery();
const { data: moduleNames } = useModuleNamesQuery(courseName);
return ( return (
<div> <div>
{moduleNames.map((m) => ( {moduleNames.map((m) => (

View File

@@ -3,15 +3,13 @@ import MonacoEditor from "@/components/MonacoEditor";
import { useQuizQuery } from "@/hooks/localCourse/quizHooks"; import { useQuizQuery } from "@/hooks/localCourse/quizHooks";
export default function EditQuiz({ export default function EditQuiz({
courseName,
moduleName, moduleName,
quizName, quizName,
}: { }: {
courseName: string;
quizName: string; quizName: string;
moduleName: string; moduleName: string;
}) { }) {
const { data: quiz } = useQuizQuery(courseName, moduleName, quizName); const { data: quiz } = useQuizQuery(moduleName, quizName);
return ( return (
<div> <div>

View File

@@ -2,15 +2,9 @@ import React from "react";
import EditQuiz from "./EditQuiz"; import EditQuiz from "./EditQuiz";
export default async function Page({ export default async function Page({
params: { courseName, moduleName, quizName }, params: { moduleName, quizName },
}: { }: {
params: { courseName: string; quizName: string; moduleName: string }; params: { quizName: string; moduleName: string };
}) { }) {
return ( return <EditQuiz quizName={quizName} moduleName={moduleName} />;
<EditQuiz
courseName={courseName}
quizName={quizName}
moduleName={moduleName}
/>
);
} }

View File

@@ -2,6 +2,7 @@ import CourseContextProvider from "./context/CourseContextProvider";
import CourseCalendar from "./calendar/CourseCalendar"; import CourseCalendar from "./calendar/CourseCalendar";
import CourseSettings from "./CourseSettings"; import CourseSettings from "./CourseSettings";
import ModuleList from "./modules/ModuleList"; import ModuleList from "./modules/ModuleList";
import DraggingContextProvider from "./context/DraggingContextProvider";
export default async function CoursePage({ export default async function CoursePage({
params: { courseName }, params: { courseName },
@@ -11,14 +12,16 @@ export default async function CoursePage({
return ( return (
<CourseContextProvider localCourseName={courseName}> <CourseContextProvider localCourseName={courseName}>
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<CourseSettings courseName={courseName} /> <CourseSettings />
<div className="flex flex-row min-h-0"> <div className="flex flex-row min-h-0">
<div className="flex-1 min-h-0"> <DraggingContextProvider localCourseName={courseName}>
<CourseCalendar /> <div className="flex-1 min-h-0">
</div> <CourseCalendar />
<div className="w-96 p-3"> </div>
<ModuleList /> <div className="w-96 p-3">
</div> <ModuleList />
</div>
</DraggingContextProvider>
</div> </div>
</div> </div>
</CourseContextProvider> </CourseContextProvider>

View File

@@ -2,12 +2,11 @@ import axios from "axios";
import { localCourseKeys } from "./localCourseKeys"; import { localCourseKeys } from "./localCourseKeys";
import { LocalAssignment } from "@/models/local/assignmnet/localAssignment"; import { LocalAssignment } from "@/models/local/assignmnet/localAssignment";
import { useSuspenseQuery, useSuspenseQueries } from "@tanstack/react-query"; import { useSuspenseQuery, useSuspenseQueries } from "@tanstack/react-query";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
export const useAssignmentNamesQuery = ( export const useAssignmentNamesQuery = (moduleName: string) => {
courseName: string, const { courseName } = useCourseContext();
moduleName: string return useSuspenseQuery({
) =>
useSuspenseQuery({
queryKey: localCourseKeys.assignmentNames(courseName, moduleName), queryKey: localCourseKeys.assignmentNames(courseName, moduleName),
queryFn: async (): Promise<string[]> => { queryFn: async (): Promise<string[]> => {
const url = const url =
@@ -20,13 +19,13 @@ export const useAssignmentNamesQuery = (
return response.data; return response.data;
}, },
}); });
};
const getAssignmentQueryConfig = ( const getAssignmentQueryConfig = (
courseName: string, courseName: string,
moduleName: string, moduleName: string,
assignmentName: string assignmentName: string
) => { ) => {
if (assignmentName === "Final Project Milestone ") console.log(moduleName);
return { return {
queryKey: localCourseKeys.assignment( queryKey: localCourseKeys.assignment(
courseName, courseName,
@@ -47,20 +46,22 @@ const getAssignmentQueryConfig = (
}; };
}; };
export const useAssignmentQuery = ( export const useAssignmentQuery = (
courseName: string,
moduleName: string, moduleName: string,
assignmentName: string assignmentName: string
) => ) => {
useSuspenseQuery( const { courseName } = useCourseContext();
return useSuspenseQuery(
getAssignmentQueryConfig(courseName, moduleName, assignmentName) getAssignmentQueryConfig(courseName, moduleName, assignmentName)
); );
};
export const useAssignmentsQueries = ( export const useAssignmentsQueries = (
courseName: string,
moduleName: string, moduleName: string,
assignmentNames: string[] assignmentNames: string[]
) => ) => {
useSuspenseQueries({ const { courseName } = useCourseContext();
return useSuspenseQueries({
queries: assignmentNames.map((name) => queries: assignmentNames.map((name) =>
getAssignmentQueryConfig(courseName, moduleName, name) getAssignmentQueryConfig(courseName, moduleName, name)
), ),
@@ -69,3 +70,4 @@ export const useAssignmentsQueries = (
pending: results.some((r) => r.isPending), pending: results.some((r) => r.isPending),
}), }),
}); });
};

View File

@@ -9,6 +9,7 @@ import {
import { usePageNamesQuery, usePagesQueries } from "./pageHooks"; import { usePageNamesQuery, usePagesQueries } from "./pageHooks";
import { useQuizNamesQuery, useQuizzesQueries } from "./quizHooks"; import { useQuizNamesQuery, useQuizzesQueries } from "./quizHooks";
import { useMemo } from "react"; import { useMemo } from "react";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
export const useLocalCourseNamesQuery = () => export const useLocalCourseNamesQuery = () =>
useSuspenseQuery({ useSuspenseQuery({
@@ -20,8 +21,9 @@ export const useLocalCourseNamesQuery = () =>
}, },
}); });
export const useLocalCourseSettingsQuery = (courseName: string) => export const useLocalCourseSettingsQuery = () => {
useSuspenseQuery({ const { courseName } = useCourseContext();
return useSuspenseQuery({
queryKey: localCourseKeys.settings(courseName), queryKey: localCourseKeys.settings(courseName),
queryFn: async (): Promise<LocalCourseSettings> => { queryFn: async (): Promise<LocalCourseSettings> => {
const url = `/api/courses/${courseName}/settings`; const url = `/api/courses/${courseName}/settings`;
@@ -29,9 +31,11 @@ export const useLocalCourseSettingsQuery = (courseName: string) =>
return response.data; return response.data;
}, },
}); });
};
export const useModuleNamesQuery = (courseName: string) => export const useModuleNamesQuery = () => {
useSuspenseQuery({ const { courseName } = useCourseContext();
return useSuspenseQuery({
queryKey: localCourseKeys.moduleNames(courseName), queryKey: localCourseKeys.moduleNames(courseName),
queryFn: async (): Promise<string[]> => { queryFn: async (): Promise<string[]> => {
const url = `/api/courses/${courseName}/modules`; const url = `/api/courses/${courseName}/modules`;
@@ -39,26 +43,24 @@ export const useModuleNamesQuery = (courseName: string) =>
return response.data; return response.data;
}, },
}); });
};
export const useModuleDataQuery = (courseName: string, moduleName: string) => { export const useModuleDataQuery = (moduleName: string) => {
const { data: assignmentNames } = useAssignmentNamesQuery( const { data: assignmentNames } = useAssignmentNamesQuery(
courseName,
moduleName moduleName
); );
const { data: quizNames } = useQuizNamesQuery(courseName, moduleName); const { data: quizNames } = useQuizNamesQuery(moduleName);
const { data: pageNames } = usePageNamesQuery(courseName, moduleName); const { data: pageNames } = usePageNamesQuery(moduleName);
const { data: assignments } = useAssignmentsQueries( const { data: assignments } = useAssignmentsQueries(
courseName,
moduleName, moduleName,
assignmentNames assignmentNames
); );
const { data: quizzes } = useQuizzesQueries( const { data: quizzes } = useQuizzesQueries(
courseName,
moduleName, moduleName,
quizNames quizNames
); );
const { data: pages } = usePagesQueries(courseName, moduleName, pageNames); const { data: pages } = usePagesQueries(moduleName, pageNames);
return useMemo( return useMemo(
() => ({ () => ({

View File

@@ -2,9 +2,11 @@ import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { useSuspenseQueries, useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQueries, useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { localCourseKeys } from "./localCourseKeys"; import { localCourseKeys } from "./localCourseKeys";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
export const usePageNamesQuery = (courseName: string, moduleName: string) => export const usePageNamesQuery = (moduleName: string) => {
useSuspenseQuery({ const { courseName } = useCourseContext();
return useSuspenseQuery({
queryKey: localCourseKeys.pageNames(courseName, moduleName), queryKey: localCourseKeys.pageNames(courseName, moduleName),
queryFn: async (): Promise<string[]> => { queryFn: async (): Promise<string[]> => {
const url = const url =
@@ -17,17 +19,14 @@ export const usePageNamesQuery = (courseName: string, moduleName: string) =>
return response.data; return response.data;
}, },
}); });
export const usePageQuery = ( };
courseName: string, export const usePageQuery = (moduleName: string, pageName: string) => {
moduleName: string, const { courseName } = useCourseContext();
pageName: string return useSuspenseQuery(getPageQueryConfig(courseName, moduleName, pageName));
) => useSuspenseQuery(getPageQueryConfig(courseName, moduleName, pageName)); };
export const usePagesQueries = ( export const usePagesQueries = (moduleName: string, pageNames: string[]) => {
courseName: string, const { courseName } = useCourseContext();
moduleName: string,
pageNames: string[]
) => {
return useSuspenseQueries({ return useSuspenseQueries({
queries: pageNames.map((name) => queries: pageNames.map((name) =>
getPageQueryConfig(courseName, moduleName, name) getPageQueryConfig(courseName, moduleName, name)

View File

@@ -7,9 +7,11 @@ import {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { localCourseKeys } from "./localCourseKeys"; import { localCourseKeys } from "./localCourseKeys";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
export const useQuizNamesQuery = (courseName: string, moduleName: string) => export const useQuizNamesQuery = (moduleName: string) => {
useSuspenseQuery({ const { courseName } = useCourseContext();
return useSuspenseQuery({
queryKey: localCourseKeys.quizNames(courseName, moduleName), queryKey: localCourseKeys.quizNames(courseName, moduleName),
queryFn: async (): Promise<string[]> => { queryFn: async (): Promise<string[]> => {
const url = const url =
@@ -22,19 +24,16 @@ export const useQuizNamesQuery = (courseName: string, moduleName: string) =>
return response.data; return response.data;
}, },
}); });
};
export const useQuizQuery = ( export const useQuizQuery = (moduleName: string, quizName: string) => {
courseName: string, const { courseName } = useCourseContext();
moduleName: string, return useSuspenseQuery(getQuizQueryConfig(courseName, moduleName, quizName));
quizName: string };
) => useSuspenseQuery(getQuizQueryConfig(courseName, moduleName, quizName));
export const useQuizzesQueries = ( export const useQuizzesQueries = (moduleName: string, quizNames: string[]) => {
courseName: string, const { courseName } = useCourseContext();
moduleName: string, return useSuspenseQueries({
quizNames: string[]
) =>
useSuspenseQueries({
queries: quizNames.map((name) => queries: quizNames.map((name) =>
getQuizQueryConfig(courseName, moduleName, name) getQuizQueryConfig(courseName, moduleName, name)
), ),
@@ -43,6 +42,7 @@ export const useQuizzesQueries = (
pending: results.some((r) => r.isPending), pending: results.some((r) => r.isPending),
}), }),
}); });
};
function getQuizQueryConfig( function getQuizQueryConfig(
courseName: string, courseName: string,
@@ -65,7 +65,9 @@ function getQuizQueryConfig(
}; };
} }
export const useUpdateQuizMutation = (courseName: string) => { export const useUpdateQuizMutation = () => {
const { courseName } = useCourseContext();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({

View File

@@ -1,293 +1,293 @@
import path from "path"; // import path from "path";
import { describe, it, expect, beforeEach } from "vitest"; // import { describe, it, expect, beforeEach } from "vitest";
import fs from "fs"; // import fs from "fs";
import { DayOfWeek, LocalCourse } from "@/models/local/localCourse"; // import { DayOfWeek, LocalCourse } from "@/models/local/localCourse";
import { AssignmentSubmissionType } from "@/models/local/assignmnet/assignmentSubmissionType"; // import { AssignmentSubmissionType } from "@/models/local/assignmnet/assignmentSubmissionType";
import { QuestionType } from "@/models/local/quiz/localQuizQuestion"; // import { QuestionType } from "@/models/local/quiz/localQuizQuestion";
import { fileStorageService } from "../fileStorage/fileStorageService"; // import { fileStorageService } from "../fileStorage/fileStorageService";
describe("FileStorageTests", () => { // describe("FileStorageTests", () => {
beforeEach(() => { // beforeEach(() => {
const storageDirectory = process.env.STORAGE_DIRECTORY ?? "/tmp/canvasManagerTests"; // const storageDirectory = process.env.STORAGE_DIRECTORY ?? "/tmp/canvasManagerTests";
if (fs.existsSync(storageDirectory)) { // if (fs.existsSync(storageDirectory)) {
fs.rmdirSync(storageDirectory, { recursive: true }); // fs.rmdirSync(storageDirectory, { recursive: true });
} // }
fs.mkdirSync(storageDirectory, { recursive: true }); // fs.mkdirSync(storageDirectory, { recursive: true });
}); // });
it("empty course can be saved and loaded", async () => { // it("empty course can be saved and loaded", async () => {
const testCourse: LocalCourse = { // const testCourse: LocalCourse = {
settings: { // settings: {
name: "test empty course", // name: "test empty course",
assignmentGroups: [], // assignmentGroups: [],
daysOfWeek: [], // daysOfWeek: [],
startDate: "07/09/2024 23:59:00", // startDate: "07/09/2024 23:59:00",
endDate: "07/09/2024 23:59:00", // endDate: "07/09/2024 23:59:00",
defaultDueTime: { hour: 1, minute: 59 }, // defaultDueTime: { hour: 1, minute: 59 },
}, // },
modules: [], // modules: [],
}; // };
await fileStorageService.saveCourseAsync(testCourse); // await fileStorageService.saveCourseAsync(testCourse);
const loadedCourses = await fileStorageService.loadSavedCourses(); // const loadedCourses = await fileStorageService.loadSavedCourses();
const loadedCourse = loadedCourses.find( // const loadedCourse = loadedCourses.find(
(c) => c.settings.name === testCourse.settings.name // (c) => c.settings.name === testCourse.settings.name
); // );
expect(loadedCourse).toEqual(testCourse); // expect(loadedCourse).toEqual(testCourse);
}); // });
it("course settings can be saved and loaded", async () => { // it("course settings can be saved and loaded", async () => {
const testCourse: LocalCourse = { // const testCourse: LocalCourse = {
settings: { // settings: {
assignmentGroups: [], // assignmentGroups: [],
name: "Test Course with settings", // name: "Test Course with settings",
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday], // daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
startDate: "07/09/2024 23:59:00", // startDate: "07/09/2024 23:59:00",
endDate: "07/09/2024 23:59:00", // endDate: "07/09/2024 23:59:00",
defaultDueTime: { hour: 1, minute: 59 }, // defaultDueTime: { hour: 1, minute: 59 },
}, // },
modules: [], // modules: [],
}; // };
await fileStorageService.saveCourseAsync(testCourse); // await fileStorageService.saveCourseAsync(testCourse);
const loadedCourses = await fileStorageService.loadSavedCourses(); // const loadedCourses = await fileStorageService.loadSavedCourses();
const loadedCourse = loadedCourses.find( // const loadedCourse = loadedCourses.find(
(c) => c.settings.name === testCourse.settings.name // (c) => c.settings.name === testCourse.settings.name
); // );
expect(loadedCourse?.settings).toEqual(testCourse.settings); // expect(loadedCourse?.settings).toEqual(testCourse.settings);
}); // });
it("empty course modules can be saved and loaded", async () => { // it("empty course modules can be saved and loaded", async () => {
const testCourse: LocalCourse = { // const testCourse: LocalCourse = {
settings: { // settings: {
name: "Test Course with modules", // name: "Test Course with modules",
assignmentGroups: [], // assignmentGroups: [],
daysOfWeek: [], // daysOfWeek: [],
startDate: "07/09/2024 23:59:00", // startDate: "07/09/2024 23:59:00",
endDate: "07/09/2024 23:59:00", // endDate: "07/09/2024 23:59:00",
defaultDueTime: { hour: 1, minute: 59 }, // defaultDueTime: { hour: 1, minute: 59 },
}, // },
modules: [ // modules: [
{ // {
name: "test module 1", // name: "test module 1",
assignments: [], // assignments: [],
quizzes: [], // quizzes: [],
pages: [], // pages: [],
}, // },
], // ],
}; // };
await fileStorageService.saveCourseAsync(testCourse); // await fileStorageService.saveCourseAsync(testCourse);
const loadedCourses = await fileStorageService.loadSavedCourses(); // const loadedCourses = await fileStorageService.loadSavedCourses();
const loadedCourse = loadedCourses.find( // const loadedCourse = loadedCourses.find(
(c) => c.settings.name === testCourse.settings.name // (c) => c.settings.name === testCourse.settings.name
); // );
expect(loadedCourse?.modules).toEqual(testCourse.modules); // expect(loadedCourse?.modules).toEqual(testCourse.modules);
}); // });
it("course modules with assignments can be saved and loaded", async () => { // it("course modules with assignments can be saved and loaded", async () => {
const testCourse: LocalCourse = { // const testCourse: LocalCourse = {
settings: { // settings: {
name: "Test Course with modules and assignments", // name: "Test Course with modules and assignments",
assignmentGroups: [], // assignmentGroups: [],
daysOfWeek: [], // daysOfWeek: [],
startDate: "07/09/2024 23:59:00", // startDate: "07/09/2024 23:59:00",
endDate: "07/09/2024 23:59:00", // endDate: "07/09/2024 23:59:00",
defaultDueTime: { hour: 1, minute: 59 }, // defaultDueTime: { hour: 1, minute: 59 },
}, // },
modules: [ // modules: [
{ // {
name: "test module 1 with assignments", // name: "test module 1 with assignments",
assignments: [ // assignments: [
{ // {
name: "test assignment", // name: "test assignment",
description: "here is the description", // description: "here is the description",
dueAt: "07/09/2024 23:59:00", // dueAt: "07/09/2024 23:59:00",
lockAt: "07/09/2024 23:59:00", // lockAt: "07/09/2024 23:59:00",
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD], // submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
localAssignmentGroupName: "Final Project", // localAssignmentGroupName: "Final Project",
rubric: [ // rubric: [
{ points: 4, label: "do task 1" }, // { points: 4, label: "do task 1" },
{ points: 2, label: "do task 2" }, // { points: 2, label: "do task 2" },
], // ],
allowedFileUploadExtensions: [], // allowedFileUploadExtensions: [],
}, // },
], // ],
quizzes: [], // quizzes: [],
pages: [], // pages: [],
}, // },
], // ],
}; // };
await fileStorageService.saveCourseAsync(testCourse); // await fileStorageService.saveCourseAsync(testCourse);
const loadedCourses = await fileStorageService.loadSavedCourses(); // const loadedCourses = await fileStorageService.loadSavedCourses();
const loadedCourse = loadedCourses.find( // const loadedCourse = loadedCourses.find(
(c) => c.settings.name === testCourse.settings.name // (c) => c.settings.name === testCourse.settings.name
); // );
expect(loadedCourse?.modules[0].assignments).toEqual( // expect(loadedCourse?.modules[0].assignments).toEqual(
testCourse.modules[0].assignments // testCourse.modules[0].assignments
); // );
}); // });
it("course modules with quizzes can be saved and loaded", async () => { // it("course modules with quizzes can be saved and loaded", async () => {
const testCourse: LocalCourse = { // const testCourse: LocalCourse = {
settings: { // settings: {
name: "Test Course with modules and quiz", // name: "Test Course with modules and quiz",
assignmentGroups: [], // assignmentGroups: [],
daysOfWeek: [], // daysOfWeek: [],
startDate: "07/09/2024 23:59:00", // startDate: "07/09/2024 23:59:00",
endDate: "07/09/2024 23:59:00", // endDate: "07/09/2024 23:59:00",
defaultDueTime: { hour: 1, minute: 59 }, // defaultDueTime: { hour: 1, minute: 59 },
}, // },
modules: [ // modules: [
{ // {
name: "test module 1 with quiz", // name: "test module 1 with quiz",
assignments: [], // assignments: [],
quizzes: [ // quizzes: [
{ // {
name: "Test Quiz", // name: "Test Quiz",
description: "quiz description", // description: "quiz description",
lockAt: "07/09/2024 12:05:00", // lockAt: "07/09/2024 12:05:00",
dueAt: "07/09/2024 12:05:00", // dueAt: "07/09/2024 12:05:00",
shuffleAnswers: true, // shuffleAnswers: true,
oneQuestionAtATime: true, // oneQuestionAtATime: true,
localAssignmentGroupName: "Assignments", // localAssignmentGroupName: "Assignments",
questions: [ // questions: [
{ // {
text: "test essay", // text: "test essay",
questionType: QuestionType.ESSAY, // questionType: QuestionType.ESSAY,
points: 1, // points: 1,
answers: [], // answers: [],
matchDistractors: [], // matchDistractors: [],
}, // },
], // ],
showCorrectAnswers: false, // showCorrectAnswers: false,
allowedAttempts: 0, // allowedAttempts: 0,
}, // },
], // ],
pages: [], // pages: [],
}, // },
], // ],
}; // };
await fileStorageService.saveCourseAsync(testCourse); // await fileStorageService.saveCourseAsync(testCourse);
const loadedCourses = await fileStorageService.loadSavedCourses(); // const loadedCourses = await fileStorageService.loadSavedCourses();
const loadedCourse = loadedCourses.find( // const loadedCourse = loadedCourses.find(
(c) => c.settings.name === testCourse.settings.name // (c) => c.settings.name === testCourse.settings.name
); // );
expect(loadedCourse?.modules[0].quizzes).toEqual( // expect(loadedCourse?.modules[0].quizzes).toEqual(
testCourse.modules[0].quizzes // testCourse.modules[0].quizzes
); // );
}); // });
it("markdown storage fully populated does not lose data", async () => { // it("markdown storage fully populated does not lose data", async () => {
const testCourse: LocalCourse = { // const testCourse: LocalCourse = {
settings: { // settings: {
name: "Test Course with lots of data", // name: "Test Course with lots of data",
assignmentGroups: [], // assignmentGroups: [],
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday], // daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
startDate: "07/09/2024 23:59:00", // startDate: "07/09/2024 23:59:00",
endDate: "07/09/2024 23:59:00", // endDate: "07/09/2024 23:59:00",
defaultDueTime: { hour: 1, minute: 59 }, // defaultDueTime: { hour: 1, minute: 59 },
}, // },
modules: [ // modules: [
{ // {
name: "new test module", // name: "new test module",
assignments: [ // assignments: [
{ // {
name: "test assignment", // name: "test assignment",
description: "here is the description", // description: "here is the description",
dueAt: "07/09/2024 23:59:00", // dueAt: "07/09/2024 23:59:00",
lockAt: "07/09/2024 23:59:00", // lockAt: "07/09/2024 23:59:00",
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD], // submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
localAssignmentGroupName: "Final Project", // localAssignmentGroupName: "Final Project",
rubric: [ // rubric: [
{ points: 4, label: "do task 1" }, // { points: 4, label: "do task 1" },
{ points: 2, label: "do task 2" }, // { points: 2, label: "do task 2" },
], // ],
allowedFileUploadExtensions: [], // allowedFileUploadExtensions: [],
}, // },
], // ],
quizzes: [ // quizzes: [
{ // {
name: "Test Quiz", // name: "Test Quiz",
description: "quiz description", // description: "quiz description",
lockAt: "07/09/2024 23:59:00", // lockAt: "07/09/2024 23:59:00",
dueAt: "07/09/2024 23:59:00", // dueAt: "07/09/2024 23:59:00",
shuffleAnswers: true, // shuffleAnswers: true,
oneQuestionAtATime: false, // oneQuestionAtATime: false,
localAssignmentGroupName: "someId", // localAssignmentGroupName: "someId",
allowedAttempts: -1, // allowedAttempts: -1,
questions: [ // questions: [
{ // {
text: "test short answer", // text: "test short answer",
questionType: QuestionType.SHORT_ANSWER, // questionType: QuestionType.SHORT_ANSWER,
points: 1, // points: 1,
answers: [], // answers: [],
matchDistractors: [], // matchDistractors: [],
}, // },
], // ],
showCorrectAnswers: false, // showCorrectAnswers: false,
}, // },
], // ],
pages: [], // pages: [],
}, // },
], // ],
}; // };
await fileStorageService.saveCourseAsync(testCourse); // await fileStorageService.saveCourseAsync(testCourse);
const loadedCourses = await fileStorageService.loadSavedCourses(); // const loadedCourses = await fileStorageService.loadSavedCourses();
const loadedCourse = loadedCourses.find( // const loadedCourse = loadedCourses.find(
(c) => c.settings.name === testCourse.settings.name // (c) => c.settings.name === testCourse.settings.name
); // );
expect(loadedCourse).toEqual(testCourse); // expect(loadedCourse).toEqual(testCourse);
}); // });
it("markdown storage can persist pages", async () => { // it("markdown storage can persist pages", async () => {
const testCourse: LocalCourse = { // const testCourse: LocalCourse = {
settings: { // settings: {
name: "Test Course with page", // name: "Test Course with page",
assignmentGroups: [], // assignmentGroups: [],
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday], // daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday],
startDate: "07/09/2024 23:59:00", // startDate: "07/09/2024 23:59:00",
endDate: "07/09/2024 23:59:00", // endDate: "07/09/2024 23:59:00",
defaultDueTime: { hour: 1, minute: 59 }, // defaultDueTime: { hour: 1, minute: 59 },
}, // },
modules: [ // modules: [
{ // {
name: "page test module", // name: "page test module",
assignments: [], // assignments: [],
quizzes: [], // quizzes: [],
pages: [ // pages: [
{ // {
name: "test page persistence", // name: "test page persistence",
dueAt: "07/09/2024 23:59:00", // dueAt: "07/09/2024 23:59:00",
text: "this is some\n## markdown\n", // text: "this is some\n## markdown\n",
}, // },
], // ],
}, // },
], // ],
}; // };
await fileStorageService.saveCourseAsync(testCourse); // await fileStorageService.saveCourseAsync(testCourse);
const loadedCourses = await fileStorageService.loadSavedCourses(); // const loadedCourses = await fileStorageService.loadSavedCourses();
const loadedCourse = loadedCourses.find( // const loadedCourse = loadedCourses.find(
(c) => c.settings.name === testCourse.settings.name // (c) => c.settings.name === testCourse.settings.name
); // );
expect(loadedCourse).toEqual(testCourse); // expect(loadedCourse).toEqual(testCourse);
}); // });
}); // });