diff --git a/nextjs/.gitignore b/nextjs/.gitignore index f73c6c9..253ad55 100644 --- a/nextjs/.gitignore +++ b/nextjs/.gitignore @@ -1,5 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +.pnpm-store/ + + # dependencies /node_modules /.pnp diff --git a/nextjs/run.sh b/nextjs/run.sh index 4eefe0d..7379441 100755 --- a/nextjs/run.sh +++ b/nextjs/run.sh @@ -10,4 +10,13 @@ docker run -it --rm \ -v ~/projects/faculty/1810/2024-fall-alex/modules:/app/storage/intro_to_web \ -v ~/projects/faculty/4850_AdvancedFE/2024-fall-alex/modules:/app/storage/advanced_frontend \ node \ - bash -c "npm i && npm run dev -- -H 0.0.0.0" + sh -c " + mkdir -p ~/.npm-global && \ + npm config set prefix '~/.npm-global' && \ + export PATH=~/.npm-global/bin:\$PATH && \ + npm install -g pnpm && \ + pnpm install && pnpm start + " + + + # bash -c "npm i -g pnpm && pnpm i && pnpm run dev -- -H 0.0.0.0" diff --git a/nextjs/src/app/api/canvas/[...rest]/route.ts b/nextjs/src/app/api/canvas/[...rest]/route.ts index 7fef3ce..083efd4 100644 --- a/nextjs/src/app/api/canvas/[...rest]/route.ts +++ b/nextjs/src/app/api/canvas/[...rest]/route.ts @@ -17,13 +17,14 @@ const getUrl = (params: { rest: string[] }, req: NextRequest) => { appendQueryParams(url, req); - return url; + return url;`` }; const proxyResponseHeaders = (response: any) => { const headers = new Headers(); Object.entries(response.headers).forEach(([key, value]) => { - headers.set(key, value as string); + if (["link", "x-rate-limit-remaining"].includes(key)) + headers.set(key, value as string); }); return headers; }; @@ -32,21 +33,19 @@ export async function GET( req: NextRequest, { params }: { params: Promise<{ rest: string[] }> } ) { - return withErrorHandling(async () => { - try { - const url = getUrl(await params, req); + try { + const url = getUrl(await params, req); - const response = await axiosClient.get(url.toString()); - - const headers = proxyResponseHeaders(response); - return new NextResponse(JSON.stringify(response.data), { headers }); - } catch (error: any) { - return new NextResponse( - JSON.stringify({ error: error.message || "Canvas GET request failed" }), - { status: error.response?.status || 500 } - ); - } - }); + const response = await axiosClient.get(url.toString()); + const headers = proxyResponseHeaders(response); + return NextResponse.json(response.data, { headers }); + } catch (error: any) { + console.log("canvas get error", error, error?.message); + return NextResponse.json( + JSON.stringify({ error: error.message || "Canvas GET request failed" }), + { status: error.response?.status || 500 } + ); + } } export async function POST( diff --git a/nextjs/src/app/course/[courseName]/calendar/day/DayTitle.tsx b/nextjs/src/app/course/[courseName]/calendar/day/DayTitle.tsx index 3753a28..83b0fac 100644 --- a/nextjs/src/app/course/[courseName]/calendar/day/DayTitle.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/day/DayTitle.tsx @@ -6,11 +6,11 @@ import NewItemForm from "../../modules/NewItemForm"; import { DraggableItem } from "../../context/drag/draggingContext"; import { useDragStyleContext } from "../../context/drag/dragStyleContext"; import { getLectureForDay } from "@/models/local/lectureUtils"; -import { trpc } from "@/services/trpc/utils"; +import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks"; export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) { const { courseName } = useCourseContext(); - const [weeks] = trpc.lectures.getLectures.useSuspenseQuery({ courseName }); + const [weeks] = useLecturesSuspenseQuery(); const { setIsDragging } = useDragStyleContext(); const todaysLecture = getLectureForDay(weeks, dayAsDate); const modal = useModal(); diff --git a/nextjs/src/app/course/[courseName]/calendar/day/useTodaysItems.tsx b/nextjs/src/app/course/[courseName]/calendar/day/useTodaysItems.tsx index 614886a..0bcc630 100644 --- a/nextjs/src/app/course/[courseName]/calendar/day/useTodaysItems.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/day/useTodaysItems.tsx @@ -33,7 +33,7 @@ export function useTodaysItems(day: string) { }[] = todaysModules ? Object.keys(todaysModules).flatMap((moduleName) => todaysModules[moduleName].assignments.map((assignment) => { - const canvasAssignment = canvasAssignments.find( + const canvasAssignment = canvasAssignments?.find( (c) => c.name === assignment.name ); return { @@ -57,7 +57,7 @@ export function useTodaysItems(day: string) { }[] = todaysModules ? Object.keys(todaysModules).flatMap((moduleName) => todaysModules[moduleName].quizzes.map((quiz) => { - const canvasQuiz = canvasQuizzes.find((q) => q.title === quiz.name); + const canvasQuiz = canvasQuizzes?.find((q) => q.title === quiz.name); return { moduleName, quiz, @@ -79,7 +79,7 @@ export function useTodaysItems(day: string) { }[] = todaysModules ? Object.keys(todaysModules).flatMap((moduleName) => todaysModules[moduleName].pages.map((page) => { - const canvasPage = canvasPages.find((p) => p.title === page.name); + const canvasPage = canvasPages?.find((p) => p.title === page.name); return { moduleName, page, diff --git a/nextjs/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx b/nextjs/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx index f05e8ae..014ff7c 100644 --- a/nextjs/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx +++ b/nextjs/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx @@ -18,9 +18,6 @@ export default function CalendarItemsContextProvider({ const { assignmentsAndModules, quizzesAndModules, pagesAndModules } = useAllCourseDataQuery(); - - - const assignmentsByModuleByDate = assignmentsAndModules.reduce( (previous, { assignment, moduleName }) => { const dueDay = getDateOnlyMarkdownString( diff --git a/nextjs/src/app/course/[courseName]/context/drag/useItemDropOnDay.ts b/nextjs/src/app/course/[courseName]/context/drag/useItemDropOnDay.ts index 2f1c43d..442259c 100644 --- a/nextjs/src/app/course/[courseName]/context/drag/useItemDropOnDay.ts +++ b/nextjs/src/app/course/[courseName]/context/drag/useItemDropOnDay.ts @@ -1,6 +1,9 @@ "use client"; import { useUpdateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks"; -import { useLectureUpdateMutation } from "@/hooks/localCourse/lectureHooks"; +import { + useLecturesSuspenseQuery, + useLectureUpdateMutation, +} from "@/hooks/localCourse/lectureHooks"; import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; import { useUpdatePageMutation } from "@/hooks/localCourse/pageHooks"; import { LocalAssignment } from "@/models/local/assignment/localAssignment"; @@ -36,9 +39,7 @@ export function useItemDropOnDay({ const [settings] = useLocalCourseSettingsQuery(); const { courseName } = useCourseContext(); // const { data: weeks } = useLecturesByWeekQuery(); - const [weeks] = trpc.lectures.getLectures.useSuspenseQuery({ - courseName: settings.name, - }); + const [weeks] = useLecturesSuspenseQuery(); const updateQuizMutation = useUpdateQuizMutation(); const updateLectureMutation = useLectureUpdateMutation(); const updateAssignmentMutation = useUpdateAssignmentMutation(); diff --git a/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx b/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx index 429f353..8c2fdfd 100644 --- a/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx +++ b/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/EditLecture.tsx @@ -1,7 +1,7 @@ "use client"; import { MonacoEditor } from "@/components/editor/MonacoEditor"; -import { useLectureUpdateMutation } from "@/hooks/localCourse/lectureHooks"; +import { useLecturesSuspenseQuery, useLectureUpdateMutation } from "@/hooks/localCourse/lectureHooks"; import { lectureToString, parseLecture, @@ -17,7 +17,7 @@ import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHoo export default function EditLecture({ lectureDay }: { lectureDay: string }) { const { courseName } = useCourseContext(); const [settings] = useLocalCourseSettingsQuery(); - const [weeks] = trpc.lectures.getLectures.useSuspenseQuery({ courseName }); + const [weeks] = useLecturesSuspenseQuery(); const updateLecture = useLectureUpdateMutation(); const lecture = weeks diff --git a/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/preview/LecturePreviewPage.tsx b/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/preview/LecturePreviewPage.tsx index f8915b3..f9a2871 100644 --- a/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/preview/LecturePreviewPage.tsx +++ b/nextjs/src/app/course/[courseName]/lecture/[lectureDay]/preview/LecturePreviewPage.tsx @@ -5,6 +5,7 @@ import { getCourseUrl, getLectureUrl } from "@/services/urlUtils"; import { useCourseContext } from "../../../context/courseContext"; import Link from "next/link"; import { trpc } from "@/services/trpc/utils"; +import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks"; export default function LecturePreviewPage({ lectureDay, @@ -12,7 +13,7 @@ export default function LecturePreviewPage({ lectureDay: string; }) { const { courseName } = useCourseContext(); - const [weeks] = trpc.lectures.getLectures.useSuspenseQuery({ courseName }); + const [weeks] = useLecturesSuspenseQuery(); const lecture = weeks .flatMap(({ lectures }) => lectures.map((lecture) => lecture)) .find((l) => l.date === lectureDay); diff --git a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx index 503d7b4..3a08863 100644 --- a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx +++ b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx @@ -21,8 +21,9 @@ import { getModuleItemUrl } from "@/services/urlUtils"; import { useCourseContext } from "../context/courseContext"; import { Expandable } from "../../../../components/Expandable"; import { useDragStyleContext } from "../context/drag/dragStyleContext"; -import { useAssignmentsQuery } from "@/hooks/localCourse/assignmentHooks"; import { useQuizzesQueries } from "@/hooks/localCourse/quizHooks"; +import { useAssignmentNamesQuery } from "@/hooks/localCourse/assignmentHooks"; +import { trpc } from "@/services/trpc/utils"; export default function ExpandableModule({ moduleName, @@ -30,8 +31,14 @@ export default function ExpandableModule({ moduleName: string; }) { const { itemDropOnModule } = useDraggingContext(); - - const [assignments ] = useAssignmentsQuery(moduleName); + const { courseName } = useCourseContext(); + const [assignmentNames] = useAssignmentNamesQuery(moduleName); + + const [assignments] = trpc.useSuspenseQueries((t) => + assignmentNames.map((assignmentName) => + t.assignment.getAssignment({ courseName, moduleName, assignmentName }) + ) + ); const [quizzes] = useQuizzesQueries(moduleName); const [pages] = usePagesQueries(moduleName); const modal = useModal(); diff --git a/nextjs/src/app/course/[courseName]/modules/ModuleCanvasStatus.tsx b/nextjs/src/app/course/[courseName]/modules/ModuleCanvasStatus.tsx index ce21479..d584fc1 100644 --- a/nextjs/src/app/course/[courseName]/modules/ModuleCanvasStatus.tsx +++ b/nextjs/src/app/course/[courseName]/modules/ModuleCanvasStatus.tsx @@ -10,7 +10,7 @@ export function ModuleCanvasStatus({ moduleName }: { moduleName: string }) { const { data: canvasModules } = useCanvasModulesQuery(); const addToCanvas = useAddCanvasModuleMutation(); - const canvasModule = canvasModules.find((c) => c.name === moduleName); + const canvasModule = canvasModules?.find((c) => c.name === moduleName); return (
diff --git a/nextjs/src/app/course/[courseName]/modules/ModuleList.tsx b/nextjs/src/app/course/[courseName]/modules/ModuleList.tsx index e438ffc..f3ed202 100644 --- a/nextjs/src/app/course/[courseName]/modules/ModuleList.tsx +++ b/nextjs/src/app/course/[courseName]/modules/ModuleList.tsx @@ -7,9 +7,9 @@ export default function ModuleList() { const [moduleNames] = useModuleNamesQuery(); return (
- {/* {moduleNames.map((m) => ( + {moduleNames.map((m) => ( - ))} */} + ))}
diff --git a/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/AssignmentButtons.tsx b/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/AssignmentButtons.tsx index 95e2580..4a49ea9 100644 --- a/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/AssignmentButtons.tsx +++ b/nextjs/src/app/course/[courseName]/modules/[moduleName]/assignment/[assignmentName]/AssignmentButtons.tsx @@ -43,7 +43,7 @@ export function AssignmentButtons({ const [isLoading, setIsLoading] = useState(false); const modal = useModal(); - const assignmentInCanvas = canvasAssignments.find( + const assignmentInCanvas = canvasAssignments?.find( (a) => a.name === assignmentName ); @@ -61,7 +61,7 @@ export function AssignmentButtons({
{anythingIsLoading && } - {assignmentInCanvas && !assignmentInCanvas.published && ( + {assignmentInCanvas && !assignmentInCanvas?.published && (
Not Published
)} {!assignmentInCanvas && ( @@ -125,8 +125,8 @@ export function AssignmentButtons({
+ {loading && }
)} diff --git a/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizButton.tsx b/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizButton.tsx index 491ed89..3b1a756 100644 --- a/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizButton.tsx +++ b/nextjs/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/QuizButton.tsx @@ -36,7 +36,7 @@ export function QuizButtons({ const deleteLocal = useDeleteQuizMutation(); const modal = useModal(); - const quizInCanvas = canvasQuizzes.find((c) => c.title === quizName); + const quizInCanvas = canvasQuizzes?.find((c) => c.title === quizName); return (
@@ -90,8 +90,8 @@ export function QuizButtons({
+ c.name} + /> ); } diff --git a/nextjs/src/app/todaysLectures/OneCourseLectures.tsx b/nextjs/src/app/todaysLectures/OneCourseLectures.tsx index aaa9e8d..68e9cf7 100644 --- a/nextjs/src/app/todaysLectures/OneCourseLectures.tsx +++ b/nextjs/src/app/todaysLectures/OneCourseLectures.tsx @@ -5,12 +5,11 @@ import { getLecturePreviewUrl } from "@/services/urlUtils"; import Link from "next/link"; import { useCourseContext } from "../course/[courseName]/context/courseContext"; import { getLectureForDay } from "@/models/local/lectureUtils"; -import { trpc } from "@/services/trpc/utils"; +import { useLecturesSuspenseQuery as useLecturesQuery } from "@/hooks/localCourse/lectureHooks"; export default function OneCourseLectures() { const { courseName } = useCourseContext(); - // const { data: weeks } = useLecturesByWeekQuery(); - const [weeks] = trpc.lectures.getLectures.useSuspenseQuery({ courseName }); + const [weeks] = useLecturesQuery(); const dayAsDate = new Date(); const dayAsString = getDateOnlyMarkdownString(dayAsDate); diff --git a/nextjs/src/hooks/canvas/canvasAssignmentHooks.ts b/nextjs/src/hooks/canvas/canvasAssignmentHooks.ts index d7cceda..d2a4920 100644 --- a/nextjs/src/hooks/canvas/canvasAssignmentHooks.ts +++ b/nextjs/src/hooks/canvas/canvasAssignmentHooks.ts @@ -1,11 +1,5 @@ import { canvasAssignmentService } from "@/services/canvas/canvasAssignmentService"; -import { canvasService } from "@/services/canvas/canvasService"; -import { - useMutation, - useQueryClient, - useSuspenseQueries, - useSuspenseQuery, -} from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useLocalCourseSettingsQuery } from "../localCourse/localCoursesHooks"; import { LocalAssignment } from "@/models/local/assignment/localAssignment"; import { canvasModuleService } from "@/services/canvas/canvasModuleService"; @@ -24,28 +18,12 @@ export const canvasAssignmentKeys = { export const useCanvasAssignmentsQuery = () => { const [settings] = useLocalCourseSettingsQuery(); - return useSuspenseQuery({ + return useQuery({ queryKey: canvasAssignmentKeys.assignments(settings.canvasId), queryFn: async () => canvasAssignmentService.getAll(settings.canvasId), }); }; -// export const useCanvasAssignmentsQuery = () => { -// const [settings] = useLocalCourseSettingsQuery(); -// const { data: allAssignments } = useInnerCanvasAssignmentsQuery(); - -// return useSuspenseQueries({ -// queries: allAssignments.map((a) => ({ -// queryKey: canvasAssignmentKeys.assignment(settings.canvasId, a.name), -// queryFn: () => a, -// })), -// combine: (results) => ({ -// data: results.map((r) => r.data), -// pending: results.some((r) => r.isPending), -// }), -// }); -// }; - export const useAddAssignmentToCanvasMutation = () => { const [settings] = useLocalCourseSettingsQuery(); const { data: canvasModules } = useCanvasModulesQuery(); @@ -60,6 +38,11 @@ export const useAddAssignmentToCanvasMutation = () => { assignment: LocalAssignment; moduleName: string; }) => { + if (!canvasModules) { + console.log("cannot add assignment until modules loaded"); + return; + } + const assignmentGroup = settings.assignmentGroups.find( (g) => g.name === assignment.localAssignmentGroupName ); diff --git a/nextjs/src/hooks/canvas/canvasCourseHooks.ts b/nextjs/src/hooks/canvas/canvasCourseHooks.ts index fbc0f6c..f340a14 100644 --- a/nextjs/src/hooks/canvas/canvasCourseHooks.ts +++ b/nextjs/src/hooks/canvas/canvasCourseHooks.ts @@ -1,7 +1,9 @@ +import { CanvasAssignmentGroup } from "@/models/canvas/assignments/canvasAssignmentGroup"; +import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel"; import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup"; import { canvasAssignmentGroupService } from "@/services/canvas/canvasAssignmentGroupService"; import { canvasService } from "@/services/canvas/canvasService"; -import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; export const canvasCourseKeys = { courseDetails: (canavasId: number) => @@ -13,34 +15,37 @@ export const canvasCourseKeys = { }; export const useCourseListInTermQuery = (canvasTermId: number | undefined) => - useSuspenseQuery({ + useQuery({ queryKey: canvasCourseKeys.courseListInTerm(canvasTermId), - queryFn: async () => + queryFn: async (): Promise => canvasTermId ? await canvasService.getCourses(canvasTermId) : [], + enabled: !!canvasTermId, }); -export const useCanvasCourseQuery = (canvasId: number) => - useSuspenseQuery({ - queryKey: canvasCourseKeys.courseDetails(canvasId), - queryFn: async () => await canvasService.getCourse(canvasId), - }); +// export const useCanvasCourseQuery = (canvasId: number) => +// useQuery({ +// queryKey: canvasCourseKeys.courseDetails(canvasId), +// queryFn: async () => await canvasService.getCourse(canvasId), +// }); export const useSetAssignmentGroupsMutation = (canvasId: number) => { const { data: canvasAssignmentGroups } = useAssignmentGroupsQuery(canvasId); return useMutation({ mutationFn: async (localAssignmentGroups: LocalAssignmentGroup[]) => { + if (!canvasAssignmentGroups) return; + const localNames = localAssignmentGroups.map((g) => g.name); const groupsToDelete = canvasAssignmentGroups.filter( - (c) => !localNames.includes(c.name) + (c: CanvasAssignmentGroup) => !localNames.includes(c.name) ); await Promise.all([ ...groupsToDelete.map( - async (g) => + async (g: CanvasAssignmentGroup) => await canvasAssignmentGroupService.delete(canvasId, g.id, g.name) ), ...localAssignmentGroups.map(async (group) => { const canvasGroup = canvasAssignmentGroups.find( - (c) => c.name === group.name + (c: CanvasAssignmentGroup) => c.name === group.name ); if (!canvasGroup) { await canvasAssignmentGroupService.create(canvasId, group); @@ -55,8 +60,8 @@ export const useSetAssignmentGroupsMutation = (canvasId: number) => { }; export const useAssignmentGroupsQuery = (canvasId: number) => { - return useSuspenseQuery({ + return useQuery({ queryKey: canvasCourseKeys.assignmentGroups(canvasId), - queryFn: async () => await canvasAssignmentGroupService.getAll(canvasId), + queryFn: async (): Promise => await canvasAssignmentGroupService.getAll(canvasId), }); }; diff --git a/nextjs/src/hooks/canvas/canvasModuleHooks.ts b/nextjs/src/hooks/canvas/canvasModuleHooks.ts index b8f8a7e..0903f3f 100644 --- a/nextjs/src/hooks/canvas/canvasModuleHooks.ts +++ b/nextjs/src/hooks/canvas/canvasModuleHooks.ts @@ -1,8 +1,8 @@ import { canvasModuleService } from "@/services/canvas/canvasModuleService"; import { useMutation, + useQuery, useQueryClient, - useSuspenseQuery, } from "@tanstack/react-query"; import { useLocalCourseSettingsQuery } from "../localCourse/localCoursesHooks"; @@ -12,7 +12,7 @@ export const canvasCourseModuleKeys = { export const useCanvasModulesQuery = () => { const [settings] = useLocalCourseSettingsQuery(); - return useSuspenseQuery({ + return useQuery({ queryKey: canvasCourseModuleKeys.modules(settings.canvasId), queryFn: async () => await canvasModuleService.getCourseModules(settings.canvasId), diff --git a/nextjs/src/hooks/canvas/canvasPageHooks.ts b/nextjs/src/hooks/canvas/canvasPageHooks.ts index 190f823..d181679 100644 --- a/nextjs/src/hooks/canvas/canvasPageHooks.ts +++ b/nextjs/src/hooks/canvas/canvasPageHooks.ts @@ -1,10 +1,6 @@ import { LocalCoursePage } from "@/models/local/page/localCoursePage"; import { canvasPageService } from "@/services/canvas/canvasPageService"; -import { - useMutation, - useQueryClient, - useSuspenseQuery, -} from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useLocalCourseSettingsQuery } from "../localCourse/localCoursesHooks"; import { canvasModuleService } from "@/services/canvas/canvasModuleService"; import { @@ -22,7 +18,7 @@ export const canvasPageKeys = { export const useCanvasPagesQuery = () => { const [settings] = useLocalCourseSettingsQuery(); - return useSuspenseQuery({ + return useQuery({ queryKey: canvasPageKeys.pagesInCourse(settings.canvasId), queryFn: async () => await canvasPageService.getAll(settings.canvasId), }); @@ -42,6 +38,10 @@ export const useCreateCanvasPageMutation = () => { page: LocalCoursePage; moduleName: string; }) => { + if (!canvasModules) { + console.log("cannot add page until modules loaded"); + return; + } const canvasPage = await canvasPageService.create( settings.canvasId, page diff --git a/nextjs/src/hooks/canvas/canvasQuizHooks.ts b/nextjs/src/hooks/canvas/canvasQuizHooks.ts index 80776c7..cdfb68c 100644 --- a/nextjs/src/hooks/canvas/canvasQuizHooks.ts +++ b/nextjs/src/hooks/canvas/canvasQuizHooks.ts @@ -1,7 +1,7 @@ import { useMutation, + useQuery, useQueryClient, - useSuspenseQuery, } from "@tanstack/react-query"; import { useLocalCourseSettingsQuery } from "../localCourse/localCoursesHooks"; import { canvasQuizService } from "@/services/canvas/canvasQuizService"; @@ -20,7 +20,7 @@ export const canvasQuizKeys = { export const useCanvasQuizzesQuery = () => { const [settings] = useLocalCourseSettingsQuery(); - return useSuspenseQuery({ + return useQuery({ queryKey: canvasQuizKeys.quizzes(settings.canvasId), queryFn: async () => canvasQuizService.getAll(settings.canvasId), }); @@ -40,6 +40,10 @@ export const useAddQuizToCanvasMutation = () => { quiz: LocalQuiz; moduleName: string; }) => { + if (!canvasModules) { + console.log("cannot add quiz until modules loaded"); + return; + } const assignmentGroup = settings.assignmentGroups.find( (g) => g.name === quiz.localAssignmentGroupName ); diff --git a/nextjs/src/hooks/hookHydration.ts b/nextjs/src/hooks/hookHydration.ts deleted file mode 100644 index 480a895..0000000 --- a/nextjs/src/hooks/hookHydration.ts +++ /dev/null @@ -1,208 +0,0 @@ -// import { QueryClient } from "@tanstack/react-query"; -// import { localCourseKeys } from "./localCourse/localCourseKeys"; -// import { fileStorageService } from "@/services/fileStorage/fileStorageService"; -// import { LocalCourseSettings } from "@/models/local/localCourseSettings"; -// import { canvasAssignmentService } from "@/services/canvas/canvasAssignmentService"; -// import { canvasAssignmentKeys } from "./canvas/canvasAssignmentHooks"; -// import { LocalAssignment } from "@/models/local/assignment/localAssignment"; -// import { LocalCoursePage } from "@/models/local/page/localCoursePage"; -// import { LocalQuiz } from "@/models/local/quiz/localQuiz"; -// import { canvasQuizService } from "@/services/canvas/canvasQuizService"; -// import { canvasPageService } from "@/services/canvas/canvasPageService"; -// import { canvasQuizKeys } from "./canvas/canvasQuizHooks"; -// import { canvasPageKeys } from "./canvas/canvasPageHooks"; -// // import { getLecturesQueryConfig } from "./localCourse/lectureHooks"; - -// // https://tanstack.com/query/latest/docs/framework/react/guides/ssr -// export const hydrateCourses = async (queryClient: QueryClient) => { -// const allSettings = await fileStorageService.settings.getAllCoursesSettings(); -// await queryClient.prefetchQuery({ -// queryKey: localCourseKeys.allCoursesSettings, -// queryFn: () => allSettings, -// }); - -// await Promise.all( -// allSettings.map(async (settings) => { -// await hydrateCourse(queryClient, settings); -// }) -// ); -// }; - -// export const hydrateCourse = async ( -// queryClient: QueryClient, -// courseSettings: LocalCourseSettings -// ) => { -// const courseName = courseSettings.name; -// const moduleNames = await fileStorageService.modules.getModuleNames( -// courseName -// ); - -// // await Promise.all( -// // moduleNames.map(async (moduleName) => { -// // const assignments = await trpcHelpers.assignment.getAllAssignments.fetch({ -// // courseName, -// // moduleName, -// // }); -// // await Promise.all( -// // assignments.map( -// // async (a) => -// // await trpcHelpers.assignment.getAssignment.fetch({ -// // courseName, -// // moduleName, -// // assignmentName: a.name, -// // }) -// // ) -// // ); -// // }) -// // ); - -// const modulesData = await Promise.all( -// moduleNames.map((moduleName) => loadAllModuleData(courseName, moduleName)) -// ); - -// // await queryClient.prefetchQuery(getLecturesQueryConfig(courseName)); - -// await queryClient.prefetchQuery({ -// queryKey: localCourseKeys.settings(courseName), -// queryFn: () => courseSettings, -// }); -// await queryClient.prefetchQuery({ -// queryKey: localCourseKeys.moduleNames(courseName), -// queryFn: () => moduleNames, -// }); - -// await Promise.all( -// modulesData.map((d) => hydrateModuleData(d, courseName, queryClient)) -// ); -// }; - -// export const hydrateCanvasCourse = async ( -// canvasCourseId: number, -// queryClient: QueryClient -// ) => { -// await Promise.all([ -// queryClient.prefetchQuery({ -// queryKey: canvasAssignmentKeys.assignments(canvasCourseId), -// queryFn: async () => await canvasAssignmentService.getAll(canvasCourseId), -// }), -// queryClient.prefetchQuery({ -// queryKey: canvasQuizKeys.quizzes(canvasCourseId), -// queryFn: async () => await canvasQuizService.getAll(canvasCourseId), -// }), -// queryClient.prefetchQuery({ -// queryKey: canvasPageKeys.pagesInCourse(canvasCourseId), -// queryFn: async () => await canvasPageService.getAll(canvasCourseId), -// }), -// ]); -// }; - -// const loadAllModuleData = async (courseName: string, moduleName: string) => { -// const [pages, quizzes] = await Promise.all([ -// // await fileStorageService.assignments.getAssignmentNames( -// // courseName, -// // moduleName -// // ), -// await fileStorageService.pages.getPages(courseName, moduleName), -// await fileStorageService.quizzes.getQuizzes(courseName, moduleName), -// ]); - -// // const [assignments] = await Promise.all([ -// // await Promise.all( -// // assignmentNames.map(async (assignmentName) => { -// // try { -// // return await fileStorageService.assignments.getAssignment( -// // courseName, -// // moduleName, -// // assignmentName -// // ); -// // } catch (error) { -// // console.error(`Error fetching assignment: ${assignmentName}`, error); -// // return null; // or any other placeholder value -// // } -// // }) -// // ), -// // ]); - -// // const assignmentsLoaded = assignments.filter((a) => a !== null); -// return { -// moduleName, -// // assignments: assignmentsLoaded, -// quizzes, -// pages, -// }; -// }; - -// const hydrateModuleData = async ( -// { -// moduleName, -// // assignments, -// quizzes, -// pages, -// }: { -// moduleName: string; -// // assignments: LocalAssignment[]; -// quizzes: LocalQuiz[]; -// pages: LocalCoursePage[]; -// }, -// courseName: string, -// queryClient: QueryClient -// ) => { -// // await queryClient.prefetchQuery({ -// // queryKey: localCourseKeys.allItemsOfType( -// // courseName, -// // moduleName, -// // "Assignment" -// // ), -// // queryFn: () => assignments, -// // }); -// await queryClient.prefetchQuery({ -// queryKey: localCourseKeys.allItemsOfType(courseName, moduleName, "Quiz"), -// queryFn: () => quizzes, -// }); -// await queryClient.prefetchQuery({ -// queryKey: localCourseKeys.allItemsOfType(courseName, moduleName, "Page"), -// queryFn: () => pages, -// }); -// // await Promise.all( -// // assignments.map( -// // async (assignment) => -// // await queryClient.prefetchQuery({ -// // queryKey: localCourseKeys.itemOfType( -// // courseName, -// // moduleName, -// // assignment.name, -// // "Assignment" -// // ), -// // queryFn: () => assignment, -// // }) -// // ) -// // ); -// await Promise.all( -// quizzes.map( -// async (quiz) => -// await queryClient.prefetchQuery({ -// queryKey: localCourseKeys.itemOfType( -// courseName, -// moduleName, -// quiz.name, -// "Quiz" -// ), -// queryFn: () => quiz, -// }) -// ) -// ); -// await Promise.all( -// pages.map( -// async (page) => -// await queryClient.prefetchQuery({ -// queryKey: localCourseKeys.itemOfType( -// courseName, -// moduleName, -// page.name, -// "Page" -// ), -// queryFn: () => page, -// }) -// ) -// ); -// }; diff --git a/nextjs/src/hooks/localCourse/assignmentHooks.ts b/nextjs/src/hooks/localCourse/assignmentHooks.ts index 5f68633..807a33d 100644 --- a/nextjs/src/hooks/localCourse/assignmentHooks.ts +++ b/nextjs/src/hooks/localCourse/assignmentHooks.ts @@ -14,13 +14,18 @@ export const useAssignmentQuery = ( }); }; -export const useAssignmentsQuery = (moduleName: string) => { +export const useAssignmentNamesQuery = (moduleName: string) => { const { courseName } = useCourseContext(); console.log("rendering all assignments query"); - return trpc.assignment.getAllAssignments.useSuspenseQuery({ - moduleName, - courseName, - }); + return trpc.assignment.getAllAssignments.useSuspenseQuery( + { + moduleName, + courseName, + }, + { + select: (assignments) => assignments.map((a) => a.name), + } + ); }; export const useUpdateAssignmentMutation = () => { @@ -57,8 +62,18 @@ export const useCreateAssignmentMutation = () => { export const useDeleteAssignmentMutation = () => { const utils = trpc.useUtils(); return trpc.assignment.deleteAssignment.useMutation({ - onSuccess: (_, { courseName, moduleName }) => { + onSuccess: (_, { courseName, moduleName, assignmentName }) => { utils.assignment.getAllAssignments.invalidate({ courseName, moduleName }); + utils.assignment.getAssignment.invalidate( + { + courseName, + moduleName, + assignmentName, + }, + { + refetchType: "all", + } + ); }, }); }; diff --git a/nextjs/src/hooks/localCourse/lectureHooks.ts b/nextjs/src/hooks/localCourse/lectureHooks.ts index fa481ac..f2d2cc0 100644 --- a/nextjs/src/hooks/localCourse/lectureHooks.ts +++ b/nextjs/src/hooks/localCourse/lectureHooks.ts @@ -1,5 +1,11 @@ +import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; import { trpc } from "@/services/trpc/utils"; +export const useLecturesSuspenseQuery = () => { + const { courseName } = useCourseContext(); + return trpc.lectures.getLectures.useSuspenseQuery({ courseName }); +}; + export const useLectureUpdateMutation = () => { const utils = trpc.useUtils(); return trpc.lectures.updateLecture.useMutation({ diff --git a/nextjs/src/hooks/localCourse/pageHooks.ts b/nextjs/src/hooks/localCourse/pageHooks.ts index 2c23450..1621f11 100644 --- a/nextjs/src/hooks/localCourse/pageHooks.ts +++ b/nextjs/src/hooks/localCourse/pageHooks.ts @@ -41,7 +41,12 @@ export const useDeletePageMutation = () => { const utils = trpc.useUtils(); return trpc.page.deletePage.useMutation({ onSuccess: (_, { courseName, moduleName, pageName }) => { - utils.page.getAllPages.invalidate({ courseName, moduleName }); + utils.page.getAllPages.invalidate( + { courseName, moduleName }, + { + refetchType: "all", + } + ); utils.page.getPage.invalidate({ courseName, moduleName, pageName }); }, }); diff --git a/nextjs/src/hooks/localCourse/quizHooks.ts b/nextjs/src/hooks/localCourse/quizHooks.ts index c1fa248..847fe2a 100644 --- a/nextjs/src/hooks/localCourse/quizHooks.ts +++ b/nextjs/src/hooks/localCourse/quizHooks.ts @@ -41,7 +41,10 @@ export const useDeleteQuizMutation = () => { const utils = trpc.useUtils(); return trpc.quiz.deleteQuiz.useMutation({ onSuccess: (_, { courseName, moduleName, quizName }) => { - utils.quiz.getAllQuizzes.invalidate({ courseName, moduleName }); + utils.quiz.getAllQuizzes.invalidate( + { courseName, moduleName }, + { refetchType: "all" } + ); utils.quiz.getQuiz.invalidate({ courseName, moduleName, quizName }); }, }); diff --git a/nextjs/src/models/local/semesterTransferUtils.ts b/nextjs/src/models/local/semesterTransferUtils.ts index 4a4cac7..f113a61 100644 --- a/nextjs/src/models/local/semesterTransferUtils.ts +++ b/nextjs/src/models/local/semesterTransferUtils.ts @@ -1,11 +1,10 @@ import { LocalAssignment } from "./assignment/localAssignment"; +import { Lecture } from "./lecture"; import { LocalCoursePage } from "./page/localCoursePage"; import { LocalQuiz } from "./quiz/localQuiz"; import { dateToMarkdownString, - getDateFromString, getDateFromStringOrThrow, - getDateOnlyMarkdownString, } from "./timeUtils"; export const prepAssignmentForNewSemester = ( @@ -69,6 +68,24 @@ export const prepPageForNewSemester = ( page.dueAt, }; }; +export const prepLectureForNewSemester = ( + lecture: Lecture, + oldSemesterStartDate: string, + newSemesterStartDate: string +): Lecture => { + const updatedText = replaceClassroomUrl(lecture.content); + const newDate = newDateOffset( + lecture.date, + oldSemesterStartDate, + newSemesterStartDate + ); + const newDateOnly = newDate?.split(" ")[0]; + return { + ...lecture, + content: updatedText, + date: newDateOnly ?? lecture.date, + }; +}; const replaceClassroomUrl = (value: string) => { const classroomPattern = diff --git a/nextjs/src/models/local/tests/testSemesterImport.test.ts b/nextjs/src/models/local/tests/testSemesterImport.test.ts index 86bcb20..eacd117 100644 --- a/nextjs/src/models/local/tests/testSemesterImport.test.ts +++ b/nextjs/src/models/local/tests/testSemesterImport.test.ts @@ -1,8 +1,14 @@ import { describe, it, expect } from "vitest"; import { LocalAssignment } from "../assignment/localAssignment"; -import { prepAssignmentForNewSemester, prepPageForNewSemester, prepQuizForNewSemester } from "../semesterTransferUtils"; +import { + prepAssignmentForNewSemester, + prepLectureForNewSemester, + prepPageForNewSemester, + prepQuizForNewSemester, +} from "../semesterTransferUtils"; import { LocalQuiz } from "../quiz/localQuiz"; import { LocalCoursePage } from "../page/localCoursePage"; +import { Lecture } from "../lecture"; describe("can take an assignment and template it for a new semester", () => { it("can sanitize assignment github classroom repo url", () => { @@ -193,3 +199,24 @@ describe("can prep pages", () => { expect(sanitizedPage.dueAt).toEqual("01/12/2024 23:59:00"); }); }); + +describe("can prep lecture", () => { + it("lecture gets new date, github url changes", () => { + const lecture: Lecture = { + name: "test title", + date: "08/30/2023", + content: "test text content", + }; + + const oldSemesterStartDate = "08/26/2023 23:59:00"; + const newSemesterStartDate = "01/08/2024 23:59:00"; + + const sanitizedLecture = prepLectureForNewSemester( + lecture, + oldSemesterStartDate, + newSemesterStartDate + ); + + expect(sanitizedLecture.date).toEqual("01/12/2024"); + }); +}); diff --git a/nextjs/src/services/fileStorage/lectureFileStorageService.ts b/nextjs/src/services/fileStorage/lectureFileStorageService.ts index 09e1ca7..dd99273 100644 --- a/nextjs/src/services/fileStorage/lectureFileStorageService.ts +++ b/nextjs/src/services/fileStorage/lectureFileStorageService.ts @@ -13,7 +13,6 @@ import { getDayOfWeek, LocalCourseSettings, } from "@/models/local/localCourseSettings"; -import { getWeekNumber } from "@/app/course/[courseName]/calendar/calendarMonthUtils"; import { getDateFromStringOrThrow } from "@/models/local/timeUtils"; export async function getLectures(courseName: string) { diff --git a/nextjs/src/services/trpc/router/settingsRouter.ts b/nextjs/src/services/trpc/router/settingsRouter.ts index b09e0df..5f6d9ab 100644 --- a/nextjs/src/services/trpc/router/settingsRouter.ts +++ b/nextjs/src/services/trpc/router/settingsRouter.ts @@ -3,6 +3,17 @@ import { z } from "zod"; import { router } from "../trpc"; import { fileStorageService } from "@/services/fileStorage/fileStorageService"; import { zodLocalCourseSettings } from "@/models/local/localCourseSettings"; +import { trpc } from "../utils"; +import { + getLectures, + updateLecture, +} from "@/services/fileStorage/lectureFileStorageService"; +import { + prepAssignmentForNewSemester, + prepLectureForNewSemester, + prepPageForNewSemester, + prepQuizForNewSemester, +} from "@/models/local/semesterTransferUtils"; export const settingsRouter = router({ allCoursesSettings: publicProcedure.query(async () => { @@ -25,6 +36,117 @@ export const settingsRouter = router({ return s; }), createCourse: publicProcedure + .input( + z.object({ + settings: zodLocalCourseSettings, + settingsFromCourseToImport: zodLocalCourseSettings.optional(), + }) + ) + .mutation(async ({ input: { settings, settingsFromCourseToImport } }) => { + await fileStorageService.settings.updateCourseSettings( + settings.name, + settings + ); + + if (settingsFromCourseToImport) { + const oldCourseName = settingsFromCourseToImport.name; + const newCourseName = settings.name; + const oldModules = await fileStorageService.modules.getModuleNames( + oldCourseName + ); + console.log( + "old course name", + oldCourseName, + "new course name", + newCourseName + ); + + console.log( + "old start date", + settingsFromCourseToImport.startDate, + "new start date", + settings.startDate + ); + await Promise.all( + oldModules.map(async (moduleName) => { + await fileStorageService.modules.createModule( + newCourseName, + moduleName + ); + + const [oldAssignments, oldQuizzes, oldPages, oldLecturesByWeek] = + await Promise.all([ + fileStorageService.assignments.getAssignments( + oldCourseName, + moduleName + ), + await fileStorageService.quizzes.getQuizzes( + oldCourseName, + moduleName + ), + await fileStorageService.pages.getPages( + oldCourseName, + moduleName + ), + await getLectures(oldCourseName), + ]); + + await Promise.all([ + ...oldAssignments.map(async (oldAssignment) => { + const newAssignment = prepAssignmentForNewSemester( + oldAssignment, + settingsFromCourseToImport.startDate, + settings.startDate, + ); + await fileStorageService.assignments.updateOrCreateAssignment({ + courseName: newCourseName, + moduleName, + assignmentName: newAssignment.name, + assignment: newAssignment, + }); + }), + ...oldQuizzes.map(async (oldQuiz) => { + const newQuiz = prepQuizForNewSemester( + oldQuiz, + settingsFromCourseToImport.startDate, + settings.startDate, + ); + await fileStorageService.quizzes.updateQuiz({ + courseName: newCourseName, + moduleName, + quizName: newQuiz.name, + quiz: newQuiz, + }); + }), + ...oldPages.map(async (oldPage) => { + const newPage = prepPageForNewSemester( + oldPage, + settingsFromCourseToImport.startDate, + settings.startDate, + ); + await fileStorageService.pages.updatePage({ + courseName: newCourseName, + moduleName, + pageName: newPage.name, + page: newPage, + }); + }), + ...oldLecturesByWeek.flatMap(async (oldLectureByWeek) => + oldLectureByWeek.lectures.map(async (oldLecture) => { + const newLecture = prepLectureForNewSemester( + oldLecture, + settingsFromCourseToImport.startDate, + settings.startDate, + ); + await updateLecture(newCourseName, settings, newLecture); + }) + ), + ]); + }) + ); + } + }), + updateSettings: publicProcedure .input( z.object({ settings: zodLocalCourseSettings, @@ -36,17 +158,4 @@ export const settingsRouter = router({ settings ); }), - updateSettings: publicProcedure - .input( - z.object({ - settings: zodLocalCourseSettings, - }) - ) - .mutation(async ({ input: { settings } }) => { - await fileStorageService.settings.updateCourseSettings( - settings.name, - settings - ); - }), - }); diff --git a/requests/assignment.http b/requests/assignment.http index 0d27ba0..ee14c9e 100644 --- a/requests/assignment.http +++ b/requests/assignment.http @@ -5,7 +5,7 @@ Authorization: Bearer {{$dotenv CANVAS_TOKEN}} ### -GET https://snow.instructure.com/api/v1/courses/871954/assignments +GET https://snow.instructure.com/api/v1/courses/1013058/assignments Authorization: Bearer {{$dotenv CANVAS_TOKEN}} diff --git a/requests/quiz.http b/requests/quiz.http index 45a175f..03bfdb3 100644 --- a/requests/quiz.http +++ b/requests/quiz.http @@ -1,5 +1,5 @@ -GET https://snow.instructure.com/api/v1/courses/958185/quizzes +GET https://snow.instructure.com/api/v1/courses/1013058/quizzes Authorization: Bearer {{$dotenv CANVAS_TOKEN}} ###