From 5d9ece63fa9ee60cf7017b5f3c1bea0bd604ccc8 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Fri, 30 Aug 2024 09:12:25 -0600 Subject: [PATCH] moving data to be held by react query --- .../src/app/api/courses/[courseName]/route.ts | 11 +-- .../courses/[courseName]/settings/route.ts | 9 ++ nextjs/src/app/api/courses/route.ts | 3 +- .../course/[courseName]/CourseSettings.tsx | 8 +- .../[courseName]/calendar/CourseCalendar.tsx | 27 +++--- .../context/CourseContextProvider.tsx | 51 +++++----- .../[courseName]/context/courseContext.ts | 16 ---- .../[courseName]/modules/ModuleList.tsx | 8 +- nextjs/src/app/course/[courseName]/page.tsx | 2 +- .../app/{course/[courseName] => }/loading.tsx | 0 nextjs/src/hooks/hookHydration.ts | 2 +- nextjs/src/hooks/localCoursesHooks.ts | 21 ++-- .../fileStorage/fileStorageService.ts | 87 +++++++++++------ ...kdownLoader.ts => courseMarkdownLoader.ts} | 95 ++++++++----------- .../fileStorage/utils/fileSystemUtils.ts | 20 ++++ 15 files changed, 185 insertions(+), 175 deletions(-) create mode 100644 nextjs/src/app/api/courses/[courseName]/settings/route.ts rename nextjs/src/app/{course/[courseName] => }/loading.tsx (100%) rename nextjs/src/services/fileStorage/utils/{couresMarkdownLoader.ts => courseMarkdownLoader.ts} (69%) create mode 100644 nextjs/src/services/fileStorage/utils/fileSystemUtils.ts diff --git a/nextjs/src/app/api/courses/[courseName]/route.ts b/nextjs/src/app/api/courses/[courseName]/route.ts index 2898b58..13d62a4 100644 --- a/nextjs/src/app/api/courses/[courseName]/route.ts +++ b/nextjs/src/app/api/courses/[courseName]/route.ts @@ -9,15 +9,6 @@ export async function PUT( console.log(updatedCourse); console.log(courseName); - await fileStorageService.saveCourseAsync(updatedCourse, previousCourse); + // await fileStorageService.saveCourseAsync(updatedCourse, previousCourse); return Response.json({}); } - -export async function GET( - request: Request, - { params: { courseName } }: { params: { courseName: string } } -) { - const courses = await fileStorageService.loadSavedCourses(); - const course = courses.find((c) => c.settings.name === courseName); - return Response.json(course); -} diff --git a/nextjs/src/app/api/courses/[courseName]/settings/route.ts b/nextjs/src/app/api/courses/[courseName]/settings/route.ts new file mode 100644 index 0000000..1d3f21c --- /dev/null +++ b/nextjs/src/app/api/courses/[courseName]/settings/route.ts @@ -0,0 +1,9 @@ +import { fileStorageService } from "@/services/fileStorage/fileStorageService"; + +export async function GET( + _request: Request, + { params: { courseName } }: { params: { courseName: string } } +) { + const settings = await fileStorageService.getCourseSettings(courseName) + return Response.json(settings); +} diff --git a/nextjs/src/app/api/courses/route.ts b/nextjs/src/app/api/courses/route.ts index 656fe54..fb9a972 100644 --- a/nextjs/src/app/api/courses/route.ts +++ b/nextjs/src/app/api/courses/route.ts @@ -1,6 +1,7 @@ import { fileStorageService } from "@/services/fileStorage/fileStorageService"; export async function GET() { - const courses = await fileStorageService.loadSavedCourses(); + const courses = await fileStorageService.getCourseNames(); + return Response.json(courses); } diff --git a/nextjs/src/app/course/[courseName]/CourseSettings.tsx b/nextjs/src/app/course/[courseName]/CourseSettings.tsx index 76c74cf..8c66eaf 100644 --- a/nextjs/src/app/course/[courseName]/CourseSettings.tsx +++ b/nextjs/src/app/course/[courseName]/CourseSettings.tsx @@ -1,8 +1,8 @@ "use client"; -import { useCourseContext } from "./context/courseContext"; +import { useLocalCourseSettingsQuery } from "@/hooks/localCoursesHooks"; -export default function CourseSettings() { - const context = useCourseContext(); - return
{context.localCourse.settings.name}
; +export default function CourseSettings({ courseName }: { courseName: string }) { + const { data: settings } = useLocalCourseSettingsQuery(courseName); + return
{settings.name}
; } diff --git a/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx b/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx index eddc44b..698a1b3 100644 --- a/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx @@ -5,18 +5,18 @@ import { getMonthsBetweenDates } from "./calendarMonthUtils"; import { CalendarMonth } from "./CalendarMonth"; export default function CourseCalendar() { - const { - localCourse: { - settings: { startDate, endDate }, - }, - } = useCourseContext(); + // const { + // localCourse: { + // settings: { startDate, endDate }, + // }, + // } = useCourseContext(); - const startDateTime = getDateFromStringOrThrow( - startDate, - "course start date" - ); - const endDateTime = getDateFromStringOrThrow(endDate, "course end date"); - const months = getMonthsBetweenDates(startDateTime, endDateTime); + // const startDateTime = getDateFromStringOrThrow( + // startDate, + // "course start date" + // ); + // const endDateTime = getDateFromStringOrThrow(endDate, "course end date"); + // const months = getMonthsBetweenDates(startDateTime, endDateTime); return (
- {months.map((month) => ( + Month Goes Here + {/* {months.map((month) => ( - ))} + ))} */}
); } diff --git a/nextjs/src/app/course/[courseName]/context/CourseContextProvider.tsx b/nextjs/src/app/course/[courseName]/context/CourseContextProvider.tsx index ba99414..e2d5556 100644 --- a/nextjs/src/app/course/[courseName]/context/CourseContextProvider.tsx +++ b/nextjs/src/app/course/[courseName]/context/CourseContextProvider.tsx @@ -2,7 +2,7 @@ import { ReactNode, useState } from "react"; import { CourseContext, DraggableItem } from "./courseContext"; import { - useLocalCourseDetailsQuery, + useLocalCourseSettingsQuery, useUpdateCourseMutation, } from "@/hooks/localCoursesHooks"; import { LocalQuiz } from "@/models/local/quiz/localQuiz"; @@ -16,8 +16,8 @@ export default function CourseContextProvider({ children: ReactNode; localCourseName: string; }) { - const { data: course } = useLocalCourseDetailsQuery(localCourseName); - const updateCourseMutation = useUpdateCourseMutation(course.settings.name); + const { data: settings } = useLocalCourseSettingsQuery(localCourseName); + const updateCourseMutation = useUpdateCourseMutation(localCourseName); const [itemBeingDragged, setItemBeingDragged] = useState< DraggableItem | undefined >(); @@ -30,34 +30,33 @@ export default function CourseContextProvider({ dueAt: dateToMarkdownString(day), }; - const localModule = course.modules.find((m) => - m.quizzes.map((q) => q.name).includes(updatedQuiz.name) - ); - if (!localModule) - console.log("could not find module for quiz ", updatedQuiz); + // const localModule = course.modules.find((m) => + // m.quizzes.map((q) => q.name).includes(updatedQuiz.name) + // ); + // if (!localModule) + // console.log("could not find module for quiz ", updatedQuiz); - const updatedCourse: LocalCourse = { - ...course, - modules: course.modules.map((m) => - m.name !== localModule?.name - ? m - : { - ...m, - quizzes: m.quizzes.map((q) => - q.name === updatedQuiz.name ? updatedQuiz : q - ), - } - ), - }; - updateCourseMutation.mutate({ - updatedCourse, - previousCourse: course, - }); + // const updatedCourse: LocalCourse = { + // ...course, + // modules: course.modules.map((m) => + // m.name !== localModule?.name + // ? m + // : { + // ...m, + // quizzes: m.quizzes.map((q) => + // q.name === updatedQuiz.name ? updatedQuiz : q + // ), + // } + // ), + // }; + // updateCourseMutation.mutate({ + // updatedCourse, + // previousCourse: course, + // }); }; return ( { setItemBeingDragged(d); }, diff --git a/nextjs/src/app/course/[courseName]/context/courseContext.ts b/nextjs/src/app/course/[courseName]/context/courseContext.ts index e1383e4..9ac2399 100644 --- a/nextjs/src/app/course/[courseName]/context/courseContext.ts +++ b/nextjs/src/app/course/[courseName]/context/courseContext.ts @@ -1,6 +1,5 @@ "use client"; import { IModuleItem } from "@/models/local/IModuleItem"; -import { LocalCourse } from "@/models/local/localCourse"; import { createContext, useContext } from "react"; export interface DraggableItem { @@ -9,27 +8,12 @@ export interface DraggableItem { } export interface CourseContextInterface { - localCourse: LocalCourse; startItemDrag: (dragging: DraggableItem) => void; endItemDrag: () => void; itemDrop: (droppedOnDay?: Date) => void; } const defaultValue: CourseContextInterface = { - localCourse: { - modules: [], - settings: { - name: "", - assignmentGroups: [], - daysOfWeek: [], - startDate: "", - endDate: "", - defaultDueTime: { - hour: 0, - minute: 0, - }, - }, - }, startItemDrag: () => {}, endItemDrag: () => {}, itemDrop: () => {}, diff --git a/nextjs/src/app/course/[courseName]/modules/ModuleList.tsx b/nextjs/src/app/course/[courseName]/modules/ModuleList.tsx index 2c8a180..22469b7 100644 --- a/nextjs/src/app/course/[courseName]/modules/ModuleList.tsx +++ b/nextjs/src/app/course/[courseName]/modules/ModuleList.tsx @@ -3,14 +3,12 @@ import { useCourseContext } from "../context/courseContext"; import ExpandableModule from "./ExpandableModule"; export default function ModuleList() { - const { - localCourse: { modules }, - } = useCourseContext(); return (
- {modules.map((m) => ( + modules here + {/* {modules.map((m) => ( - ))} + ))} */}
); } diff --git a/nextjs/src/app/course/[courseName]/page.tsx b/nextjs/src/app/course/[courseName]/page.tsx index 1e023a9..e41ed81 100644 --- a/nextjs/src/app/course/[courseName]/page.tsx +++ b/nextjs/src/app/course/[courseName]/page.tsx @@ -15,7 +15,7 @@ export default async function CoursePage({
- +
diff --git a/nextjs/src/app/course/[courseName]/loading.tsx b/nextjs/src/app/loading.tsx similarity index 100% rename from nextjs/src/app/course/[courseName]/loading.tsx rename to nextjs/src/app/loading.tsx diff --git a/nextjs/src/hooks/hookHydration.ts b/nextjs/src/hooks/hookHydration.ts index 2e088ba..a6601b8 100644 --- a/nextjs/src/hooks/hookHydration.ts +++ b/nextjs/src/hooks/hookHydration.ts @@ -5,6 +5,6 @@ import { fileStorageService } from "@/services/fileStorage/fileStorageService"; export const hydrateCourses = async (queryClient: QueryClient) => { await queryClient.prefetchQuery({ queryKey: localCourseKeys.allCourses, - queryFn: async () => await fileStorageService.loadSavedCourses(), + queryFn: async () => await fileStorageService.getCourseNames(), }); }; diff --git a/nextjs/src/hooks/localCoursesHooks.ts b/nextjs/src/hooks/localCoursesHooks.ts index 1e66276..772ebe0 100644 --- a/nextjs/src/hooks/localCoursesHooks.ts +++ b/nextjs/src/hooks/localCoursesHooks.ts @@ -1,4 +1,4 @@ -import { LocalCourse } from "@/models/local/localCourse"; +import { LocalCourse, LocalCourseSettings } from "@/models/local/localCourse"; import { useMutation, useQueryClient, @@ -8,30 +8,29 @@ import axios from "axios"; export const localCourseKeys = { allCourses: ["all courses"] as const, - courseDetail: (courseName: string) => ["course details", courseName] as const, + courseSettings: (courseName: string) => + ["course details", courseName, "settings"] as const, }; export const useLocalCourseNamesQuery = () => useSuspenseQuery({ queryKey: localCourseKeys.allCourses, - queryFn: async (): Promise => { + queryFn: async (): Promise => { const url = `/api/courses`; const response = await axios.get(url); return response.data; }, - select: (courses) => courses.map((c) => c.settings.name), }); -export const useLocalCourseDetailsQuery = (courseName: string) => { - return useSuspenseQuery({ - queryKey: localCourseKeys.courseDetail(courseName), - queryFn: async (): Promise => { - const url = `/api/courses/${courseName}`; +export const useLocalCourseSettingsQuery = (courseName: string) => + useSuspenseQuery({ + queryKey: localCourseKeys.courseSettings(courseName), + queryFn: async (): Promise => { + const url = `/api/courses/${courseName}/settings`; const response = await axios.get(url); return response.data; }, }); -}; export const useUpdateCourseMutation = (courseName: string) => { const queryClient = useQueryClient(); @@ -45,7 +44,7 @@ export const useUpdateCourseMutation = (courseName: string) => { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: localCourseKeys.courseDetail(courseName), + queryKey: localCourseKeys.courseSettings(courseName), }); }, scope: { diff --git a/nextjs/src/services/fileStorage/fileStorageService.ts b/nextjs/src/services/fileStorage/fileStorageService.ts index 3083c2a..6ee284a 100644 --- a/nextjs/src/services/fileStorage/fileStorageService.ts +++ b/nextjs/src/services/fileStorage/fileStorageService.ts @@ -1,27 +1,70 @@ import { promises as fs } from "fs"; import path from "path"; -import { LocalCourse } from "@/models/local/localCourse"; -import { courseMarkdownLoader } from "./utils/couresMarkdownLoader"; +import { + LocalCourse, + LocalCourseSettings, + localCourseYamlUtils, +} from "@/models/local/localCourse"; +import { courseMarkdownLoader } from "./utils/courseMarkdownLoader"; import { courseMarkdownSaver } from "./utils/courseMarkdownSaver"; +import { + directoryOrFileExists, + hasFileSystemEntries, +} from "./utils/fileSystemUtils"; const basePath = process.env.STORAGE_DIRECTORY ?? "./storage"; console.log("base path", basePath); export const fileStorageService = { - async saveCourseAsync( - course: LocalCourse, - previouslyStoredCourse?: LocalCourse - ) { - await courseMarkdownSaver.save(course, previouslyStoredCourse); - }, + // async saveCourseAsync( + // course: LocalCourse, + // previouslyStoredCourse?: LocalCourse + // ) { + // await courseMarkdownSaver.save(course, previouslyStoredCourse); + // }, - async loadSavedCourses(): Promise { - console.log("loading pages from file system"); - return (await courseMarkdownLoader.loadSavedCourses()) || []; + // async loadSavedCourses(): Promise { + // console.log("loading pages from file system"); + // return (await courseMarkdownLoader.loadSavedCourses()) || []; + // }, + + async getCourseNames() { + console.log("loading course ids"); + const courseDirectories = await fs.readdir(basePath, { + withFileTypes: true, + }); + const coursePromises = courseDirectories + .filter((dirent) => dirent.isDirectory()) + .filter(async (dirent) => { + const coursePath = path.join(basePath, dirent.name); + const settingsPath = path.join(coursePath, "settings.yml"); + return await directoryOrFileExists(settingsPath); + }); + const courseNamesFromDirectories = (await Promise.all(coursePromises)).map( + (c) => c.name + ); + + return courseNamesFromDirectories; + }, + + async getCourseSettings(courseName: string): Promise { + const courseDirectory = path.join(basePath, courseName); + const settingsPath = path.join(courseDirectory, "settings.yml"); + if (!(await directoryOrFileExists(settingsPath))) { + const errorMessage = `Error loading settings for ${courseName}, settings file ${settingsPath}`; + console.log(errorMessage); + throw new Error(errorMessage); + } + + const settingsString = await fs.readFile(settingsPath, "utf-8"); + const settings = localCourseYamlUtils.parseSettingYaml(settingsString); + + const folderName = path.basename(courseDirectory); + return { ...settings, name: folderName }; }, async getEmptyDirectories(): Promise { - if (!(await this.directoryExists(basePath))) { + if (!(await directoryOrFileExists(basePath))) { throw new Error( `Cannot get empty directories, ${basePath} does not exist` ); @@ -31,26 +74,8 @@ export const fileStorageService = { const emptyDirectories = directories .filter((dirent) => dirent.isDirectory()) .map((dirent) => path.join(basePath, dirent.name)) - .filter(async (dir) => !(await this.hasFileSystemEntries(dir))); + .filter(async (dir) => !(await hasFileSystemEntries(dir))); return emptyDirectories; }, - - async directoryExists(directoryPath: string): Promise { - try { - await fs.access(directoryPath); - return true; - } catch { - return false; - } - }, - - async hasFileSystemEntries(directoryPath: string): Promise { - try { - const entries = await fs.readdir(directoryPath); - return entries.length > 0; - } catch { - return false; - } - }, }; diff --git a/nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts b/nextjs/src/services/fileStorage/utils/courseMarkdownLoader.ts similarity index 69% rename from nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts rename to nextjs/src/services/fileStorage/utils/courseMarkdownLoader.ts index f8b01d0..a073a21 100644 --- a/nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts +++ b/nextjs/src/services/fileStorage/utils/courseMarkdownLoader.ts @@ -18,54 +18,55 @@ import { } from "@/models/local/quiz/localQuiz"; import { promises as fs } from "fs"; import path from "path"; +import { directoryOrFileExists } from "./fileSystemUtils"; const basePath = process.env.STORAGE_DIRECTORY ?? "./storage"; export const courseMarkdownLoader = { - async loadSavedCourses(): Promise { - const courseDirectories = await fs.readdir(basePath, { - withFileTypes: true, - }); - const coursePromises = courseDirectories - .filter((dirent) => dirent.isDirectory()) - .map(async (dirent) => { - const coursePath = path.join(basePath, dirent.name); - const settingsPath = path.join(coursePath, "settings.yml"); - if (await this.fileExists(settingsPath)) { - return this.loadCourseByPath(coursePath); - } - return null; - }); + // async loadSavedCourses(): Promise { + // const courseDirectories = await fs.readdir(basePath, { + // withFileTypes: true, + // }); + // const coursePromises = courseDirectories + // .filter((dirent) => dirent.isDirectory()) + // .map(async (dirent) => { + // const coursePath = path.join(basePath, dirent.name); + // const settingsPath = path.join(coursePath, "settings.yml"); + // if (await directoryOrFileExists(settingsPath)) { + // return this.loadCourseByPath(coursePath); + // } + // return null; + // }); - const courses = (await Promise.all(coursePromises)).filter( - (course) => course !== null - ) as LocalCourse[]; - return courses.sort((a, b) => - a.settings.name.localeCompare(b.settings.name) - ); - }, + // const courses = (await Promise.all(coursePromises)).filter( + // (course) => course !== null + // ) as LocalCourse[]; + // return courses.sort((a, b) => + // a.settings.name.localeCompare(b.settings.name) + // ); + // }, - async loadCourseByPath(courseDirectory: string): Promise { - if (!(await this.directoryExists(courseDirectory))) { - const errorMessage = `Error loading course by name, could not find folder ${courseDirectory}`; - console.log(errorMessage); - throw new Error(errorMessage); - } + // async loadCourseByPath(courseDirectory: string): Promise { + // if (!(await directoryOrFileExists(courseDirectory))) { + // const errorMessage = `Error loading course by name, could not find folder ${courseDirectory}`; + // console.log(errorMessage); + // throw new Error(errorMessage); + // } - const settings = await this.loadCourseSettings(courseDirectory); - const modules = await this.loadCourseModules(courseDirectory); + // const settings = await this.loadCourseSettings(courseDirectory); + // const modules = await this.loadCourseModules(courseDirectory); - return { - settings, - modules, - }; - }, + // return { + // settings, + // modules, + // }; + // }, async loadCourseSettings( courseDirectory: string ): Promise { const settingsPath = path.join(courseDirectory, "settings.yml"); - if (!(await this.fileExists(settingsPath))) { + if (!(await directoryOrFileExists(settingsPath))) { const errorMessage = `Error loading course by name, settings file ${settingsPath}`; console.log(errorMessage); throw new Error(errorMessage); @@ -110,7 +111,7 @@ export const courseMarkdownLoader = { modulePath: string ): Promise { const assignmentsPath = path.join(modulePath, "assignments"); - if (!(await this.directoryExists(assignmentsPath))) { + if (!(await directoryOrFileExists(assignmentsPath))) { console.log( `Error loading course by name, assignments folder does not exist in ${modulePath}` ); @@ -132,7 +133,7 @@ export const courseMarkdownLoader = { async loadQuizzesFromPath(modulePath: string): Promise { const quizzesPath = path.join(modulePath, "quizzes"); - if (!(await this.directoryExists(quizzesPath))) { + if (!(await directoryOrFileExists(quizzesPath))) { console.log( `Quizzes folder does not exist in ${modulePath}, creating now` ); @@ -156,7 +157,7 @@ export const courseMarkdownLoader = { modulePath: string ): Promise { const pagesPath = path.join(modulePath, "pages"); - if (!(await this.directoryExists(pagesPath))) { + if (!(await directoryOrFileExists(pagesPath))) { console.log(`Pages folder does not exist in ${modulePath}, creating now`); await fs.mkdir(pagesPath); } @@ -173,22 +174,4 @@ export const courseMarkdownLoader = { return await Promise.all(pagePromises); }, - - async fileExists(filePath: string): Promise { - try { - await fs.access(filePath); - return true; - } catch { - return false; - } - }, - - async directoryExists(directoryPath: string): Promise { - try { - await fs.access(directoryPath); - return true; - } catch { - return false; - } - }, }; diff --git a/nextjs/src/services/fileStorage/utils/fileSystemUtils.ts b/nextjs/src/services/fileStorage/utils/fileSystemUtils.ts new file mode 100644 index 0000000..7b13925 --- /dev/null +++ b/nextjs/src/services/fileStorage/utils/fileSystemUtils.ts @@ -0,0 +1,20 @@ +import { promises as fs } from "fs"; + +export const hasFileSystemEntries = async ( + directoryPath: string +): Promise => { + try { + const entries = await fs.readdir(directoryPath); + return entries.length > 0; + } catch { + return false; + } +}; +export const directoryOrFileExists = async (directoryPath: string): Promise => { + try { + await fs.access(directoryPath); + return true; + } catch { + return false; + } +}; \ No newline at end of file