diff --git a/nextjs/package-lock.json b/nextjs/package-lock.json index d709a3a..0b66818 100644 --- a/nextjs/package-lock.json +++ b/nextjs/package-lock.json @@ -18,6 +18,7 @@ "yaml": "^2.5.0" }, "devDependencies": { + "@tanstack/react-query-devtools": "^5.53.1", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.0.0", "@types/node": "^22", @@ -1531,9 +1532,20 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.52.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.52.2.tgz", - "integrity": "sha512-9vvbFecK4A0nDnrc/ks41e3UHONF1DAnGz8Tgbxkl59QcvKWmc0ewhYuIKRh8NC4ja5LTHT9EH16KHbn2AIYWA==", + "version": "5.53.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.53.1.tgz", + "integrity": "sha512-mvLG7s4Zy3Yvc2LsKm8BVafbmPrsReKgqwhmp4XKVmRW9us3KbWRqu3qBBfhVavcUUEHfNK7PvpTchvQpVdFpw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.52.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.52.3.tgz", + "integrity": "sha512-oGX9qJuNpr4vOQyeksqHr+FgLQGs5UooK87R1wTtcsUUdrRKGSgs3cBllZMtWBJxg+yVvg0TlHNGYLMjvqX3GA==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -1541,12 +1553,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.52.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.2.tgz", - "integrity": "sha512-d4OwmobpP+6+SvuAxW1RzAY95Pv87Gu+0GjtErzFOUXo+n0FGcwxKvzhswCsXKxsgnAr3bU2eJ2u+GXQAutkCQ==", + "version": "5.53.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.53.1.tgz", + "integrity": "sha512-35HU4836Ey1/W74BxmS8p9KHXcDRGPdkw6w3VX0Tc5S9v5acFl80oi/yc6nsmoLhu68wQkWMyX0h7y7cOtY5OA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.52.2" + "@tanstack/query-core": "5.53.1" }, "funding": { "type": "github", @@ -1556,6 +1568,24 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.53.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.53.1.tgz", + "integrity": "sha512-AjShRLM3/9Rglgeo0X52M8MKPEvcNnFQvs3yZq8ExQWu8YhZMzqVsFVn4PqOeyEHbnsRS2bmi0jPP/tBrlWU0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.52.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.53.1", + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", diff --git a/nextjs/package.json b/nextjs/package.json index 49bc99f..4382c01 100644 --- a/nextjs/package.json +++ b/nextjs/package.json @@ -20,6 +20,7 @@ "yaml": "^2.5.0" }, "devDependencies": { + "@tanstack/react-query-devtools": "^5.53.1", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.0.0", "@types/node": "^22", diff --git a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/quizzes/[quizName]/route.ts b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/quizzes/[quizName]/route.ts index 287a9ff..e9b1fff 100644 --- a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/quizzes/[quizName]/route.ts +++ b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/quizzes/[quizName]/route.ts @@ -6,10 +6,26 @@ export async function GET( params: { courseName, moduleName, quizName }, }: { params: { courseName: string; moduleName: string; quizName: string } } ) { - const settings = await fileStorageService.getQuiz( + const quiz = await fileStorageService.getQuiz( courseName, moduleName, quizName ); - return Response.json(settings); + return Response.json(quiz); +} + +export async function PUT( + request: Request, + { + params: { courseName, moduleName, quizName }, + }: { params: { courseName: string; moduleName: string; quizName: string } } +) { + const quiz = await request.json() + await fileStorageService.updateQuiz( + courseName, + moduleName, + quizName, + quiz + ); + return Response.json({}); } diff --git a/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx b/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx index 150278e..d7b8546 100644 --- a/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/CourseCalendar.tsx @@ -30,7 +30,6 @@ export default function CourseCalendar() { bg-slate-950 " > - Month Goes Here {months.map((month) => ( ))} diff --git a/nextjs/src/app/course/[courseName]/calendar/DayItemsInModule.tsx b/nextjs/src/app/course/[courseName]/calendar/DayItemsInModule.tsx index 75f053b..58ab8b0 100644 --- a/nextjs/src/app/course/[courseName]/calendar/DayItemsInModule.tsx +++ b/nextjs/src/app/course/[courseName]/calendar/DayItemsInModule.tsx @@ -54,7 +54,13 @@ export default function DayItemsInModule({ key={q.name} role="button" draggable="true" - onDragStart={() => startItemDrag({ type: "quiz", item: q })} + onDragStart={() => + startItemDrag({ + type: "quiz", + item: q, + sourceModuleName: moduleName, + }) + } onDragEnd={endItemDrag} > {q.name} @@ -65,7 +71,13 @@ export default function DayItemsInModule({ key={p.name} role="button" draggable="true" - onDragStart={() => startItemDrag({ type: "page", item: p })} + onDragStart={() => + startItemDrag({ + type: "page", + item: p, + sourceModuleName: moduleName, + }) + } > {p.name} diff --git a/nextjs/src/app/course/[courseName]/context/CourseContextProvider.tsx b/nextjs/src/app/course/[courseName]/context/CourseContextProvider.tsx index 57ab783..58f7b40 100644 --- a/nextjs/src/app/course/[courseName]/context/CourseContextProvider.tsx +++ b/nextjs/src/app/course/[courseName]/context/CourseContextProvider.tsx @@ -3,6 +3,7 @@ import { ReactNode, useState } from "react"; import { CourseContext, DraggableItem } from "./courseContext"; import { LocalQuiz } from "@/models/local/quiz/localQuiz"; import { dateToMarkdownString } from "@/models/local/timeUtils"; +import { useUpdateQuizMutation } from "@/hooks/localCourse/quizHooks"; export default function CourseContextProvider({ localCourseName, @@ -11,6 +12,7 @@ export default function CourseContextProvider({ children: ReactNode; localCourseName: string; }) { + const updateQuizMutation = useUpdateQuizMutation(localCourseName); const [itemBeingDragged, setItemBeingDragged] = useState< DraggableItem | undefined >(); @@ -58,10 +60,17 @@ export default function CourseContextProvider({ setItemBeingDragged(undefined); }, itemDrop: (day) => { - console.log("dropping"); if (itemBeingDragged && day) { if (itemBeingDragged.type === "quiz") { - updateQuiz(day); + const quiz: LocalQuiz = { + ...(itemBeingDragged.item as LocalQuiz), + dueAt: dateToMarkdownString(day), + }; + updateQuizMutation.mutate({ + quiz: quiz, + quizName: quiz.name, + moduleName: itemBeingDragged.sourceModuleName, + }); } } setItemBeingDragged(undefined); diff --git a/nextjs/src/app/course/[courseName]/context/courseContext.ts b/nextjs/src/app/course/[courseName]/context/courseContext.ts index a7bebb4..3c3f473 100644 --- a/nextjs/src/app/course/[courseName]/context/courseContext.ts +++ b/nextjs/src/app/course/[courseName]/context/courseContext.ts @@ -4,6 +4,7 @@ import { createContext, useContext } from "react"; export interface DraggableItem { item: IModuleItem; + sourceModuleName: string; type: "quiz" | "assignment" | "page"; } diff --git a/nextjs/src/app/providers.tsx b/nextjs/src/app/providers.tsx index aefb57c..727d8b5 100644 --- a/nextjs/src/app/providers.tsx +++ b/nextjs/src/app/providers.tsx @@ -5,6 +5,7 @@ import { QueryClientProvider, } from "@tanstack/react-query"; import { ReactNode } from "react"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; function makeQueryClient() { return new QueryClient({ @@ -39,10 +40,13 @@ export default function Providers({ children }: { children: ReactNode }) { // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary - + const queryClient = getQueryClient(); return ( - {children} + + + {children} + ); } diff --git a/nextjs/src/hooks/localCourse/localCourseKeys.ts b/nextjs/src/hooks/localCourse/localCourseKeys.ts index e142f01..7e8baf7 100644 --- a/nextjs/src/hooks/localCourse/localCourseKeys.ts +++ b/nextjs/src/hooks/localCourse/localCourseKeys.ts @@ -16,11 +16,26 @@ export const localCourseKeys = { "modules", moduleName, "assignments", + { type: "names" }, ] as const, quizNames: (courseName: string, moduleName: string) => - ["course details", courseName, "modules", moduleName, "quizzes"] as const, + [ + "course details", + courseName, + "modules", + moduleName, + "quizzes", + { type: "names" }, + ] as const, pageNames: (courseName: string, moduleName: string) => - ["course details", courseName, "modules", moduleName, "pages"] as const, + [ + "course details", + courseName, + "modules", + moduleName, + "pages", + { type: "names" }, + ] as const, assignment: ( courseName: string, moduleName: string, diff --git a/nextjs/src/hooks/localCourse/quizHooks.ts b/nextjs/src/hooks/localCourse/quizHooks.ts index c1aaab9..21659e6 100644 --- a/nextjs/src/hooks/localCourse/quizHooks.ts +++ b/nextjs/src/hooks/localCourse/quizHooks.ts @@ -1,5 +1,10 @@ import { LocalQuiz } from "@/models/local/quiz/localQuiz"; -import { useSuspenseQueries, useSuspenseQuery } from "@tanstack/react-query"; +import { + useMutation, + useQueryClient, + useSuspenseQueries, + useSuspenseQuery, +} from "@tanstack/react-query"; import axios from "axios"; import { localCourseKeys } from "./localCourseKeys"; @@ -48,3 +53,29 @@ function getQuizQueryConfig( }, }; } + +export const useUpdateQuizMutation = (courseName: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + quiz, + moduleName, + quizName, + }: { + quiz: LocalQuiz; + moduleName: string; + quizName: string; + }) => { + const url = `/api/courses/${courseName}/modules/${moduleName}/quizzes/${quizName}`; + await axios.put(url, quiz); + }, + onSuccess: (_, { moduleName, quizName }) => { + queryClient.invalidateQueries({ + queryKey: localCourseKeys.quiz(courseName, moduleName, quizName), + }); + // queryClient.invalidateQueries({ + // queryKey: localCourseKeys.quizNames(courseName, moduleName), + // }); + }, + }); +}; diff --git a/nextjs/src/services/fileStorage/fileStorageService.ts b/nextjs/src/services/fileStorage/fileStorageService.ts index bce45f6..77ee238 100644 --- a/nextjs/src/services/fileStorage/fileStorageService.ts +++ b/nextjs/src/services/fileStorage/fileStorageService.ts @@ -5,15 +5,14 @@ import { LocalCourseSettings, localCourseYamlUtils, } from "@/models/local/localCourse"; -import { courseMarkdownLoader } from "./utils/courseMarkdownLoader"; -import { courseMarkdownSaver } from "./utils/courseMarkdownSaver"; import { directoryOrFileExists, hasFileSystemEntries, } from "./utils/fileSystemUtils"; import { localAssignmentMarkdown } from "@/models/local/assignmnet/localAssignment"; -import { localQuizMarkdownUtils } from "@/models/local/quiz/localQuiz"; +import { LocalQuiz, localQuizMarkdownUtils } from "@/models/local/quiz/localQuiz"; import { localPageMarkdownUtils } from "@/models/local/page/localCoursePage"; +import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils"; const basePath = process.env.STORAGE_DIRECTORY ?? "./storage"; console.log("base path", basePath); @@ -90,8 +89,9 @@ export const fileStorageService = { } const assignmentFiles = await fs.readdir(filePath); - return assignmentFiles; + return assignmentFiles.map(f => f.replace(/\.md$/, '')); }, + async getQuizNames(courseName: string, moduleName: string) { const filePath = path.join(basePath, courseName, moduleName, "quizzes"); if (!(await directoryOrFileExists(filePath))) { @@ -102,8 +102,9 @@ export const fileStorageService = { } const files = await fs.readdir(filePath); - return files; + return files.map(f => f.replace(/\.md$/, '')); }, + async getPageNames(courseName: string, moduleName: string) { const filePath = path.join(basePath, courseName, moduleName, "pages"); if (!(await directoryOrFileExists(filePath))) { @@ -114,7 +115,7 @@ export const fileStorageService = { } const files = await fs.readdir(filePath); - return files; + return files.map(f => f.replace(/\.md$/, '')); }, async getAssignment( @@ -127,7 +128,7 @@ export const fileStorageService = { courseName, moduleName, "assignments", - assignmentName + assignmentName + ".md" ); const rawFile = (await fs.readFile(filePath, "utf-8")).replace( /\r\n/g, @@ -142,7 +143,7 @@ export const fileStorageService = { courseName, moduleName, "quizzes", - quizName + quizName + ".md" ); const rawFile = (await fs.readFile(filePath, "utf-8")).replace( /\r\n/g, @@ -151,13 +152,27 @@ export const fileStorageService = { return localQuizMarkdownUtils.parseMarkdown(rawFile); }, + async updateQuiz(courseName: string, moduleName: string, quizName: string, quiz: LocalQuiz) { + const filePath = path.join( + basePath, + courseName, + moduleName, + "quizzes", + quizName+".md" + ); + + const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz); + console.log(`Saving quiz ${filePath}`); + await fs.writeFile(filePath, quizMarkdown); + }, + async getPage(courseName: string, moduleName: string, pageName: string) { const filePath = path.join( basePath, courseName, moduleName, "pages", - pageName + pageName + ".md" ); const rawFile = (await fs.readFile(filePath, "utf-8")).replace( /\r\n/g, diff --git a/nextjs/src/services/fileStorage/utils/courseMarkdownSaver.ts b/nextjs/src/services/fileStorage/utils/courseMarkdownSaver.ts index b66f138..5d049a2 100644 --- a/nextjs/src/services/fileStorage/utils/courseMarkdownSaver.ts +++ b/nextjs/src/services/fileStorage/utils/courseMarkdownSaver.ts @@ -23,69 +23,69 @@ const saveSettings = async (course: LocalCourse, courseDirectory: string) => { await fs.writeFile(settingsFilePath, settingsYaml); }; -const saveModules = async ( - course: LocalCourse, - courseDirectory: string, - previouslyStoredCourse?: LocalCourse -) => { - for (const localModule of course.modules) { - const moduleDirectory = path.join(courseDirectory, localModule.name); - if (!(await directoryExists(moduleDirectory))) { - await fs.mkdir(moduleDirectory, { recursive: true }); - } +// const saveModules = async ( +// course: LocalCourse, +// courseDirectory: string, +// previouslyStoredCourse?: LocalCourse +// ) => { +// for (const localModule of course.modules) { +// const moduleDirectory = path.join(courseDirectory, localModule.name); +// if (!(await directoryExists(moduleDirectory))) { +// await fs.mkdir(moduleDirectory, { recursive: true }); +// } - await saveQuizzes(course, localModule, previouslyStoredCourse); - await saveAssignments(course, localModule, previouslyStoredCourse); - await savePages(course, localModule, previouslyStoredCourse); - } +// await saveQuizzes(course, localModule, previouslyStoredCourse); +// await saveAssignments(course, localModule, previouslyStoredCourse); +// await savePages(course, localModule, previouslyStoredCourse); +// } - const moduleNames = course.modules.map((m) => m.name); - const moduleDirectories = await fs.readdir(courseDirectory, { - withFileTypes: true, - }); +// const moduleNames = course.modules.map((m) => m.name); +// const moduleDirectories = await fs.readdir(courseDirectory, { +// withFileTypes: true, +// }); - for (const dirent of moduleDirectories) { - if (dirent.isDirectory() && !moduleNames.includes(dirent.name)) { - const moduleDirPath = path.join(courseDirectory, dirent.name); - console.log( - `Deleting extra module directory, it was probably renamed ${moduleDirPath}` - ); - await fs.rmdir(moduleDirPath, { recursive: true }); - } - } -}; +// for (const dirent of moduleDirectories) { +// if (dirent.isDirectory() && !moduleNames.includes(dirent.name)) { +// const moduleDirPath = path.join(courseDirectory, dirent.name); +// console.log( +// `Deleting extra module directory, it was probably renamed ${moduleDirPath}` +// ); +// await fs.rmdir(moduleDirPath, { recursive: true }); +// } +// } +// }; -const saveQuizzes = async ( - course: LocalCourse, - module: LocalModule, - previouslyStoredCourse?: LocalCourse -) => { - const quizzesDirectory = path.join( - basePath, - course.settings.name, - module.name, - "quizzes" - ); - if (!(await directoryExists(quizzesDirectory))) { - await fs.mkdir(quizzesDirectory, { recursive: true }); - } +// const saveQuizzes = async ( +// course: LocalCourse, +// module: LocalModule, +// previouslyStoredCourse?: LocalCourse +// ) => { +// const quizzesDirectory = path.join( +// basePath, +// course.settings.name, +// module.name, +// "quizzes" +// ); +// if (!(await directoryExists(quizzesDirectory))) { +// await fs.mkdir(quizzesDirectory, { recursive: true }); +// } - for (const quiz of module.quizzes) { - const previousModule = previouslyStoredCourse?.modules.find( - (m) => m.name === module.name - ); - const previousQuiz = previousModule?.quizzes.find((q) => q === quiz); +// for (const quiz of module.quizzes) { +// const previousModule = previouslyStoredCourse?.modules.find( +// (m) => m.name === module.name +// ); +// const previousQuiz = previousModule?.quizzes.find((q) => q === quiz); - if (!previousQuiz) { - const markdownPath = path.join(quizzesDirectory, `${quiz.name}.md`); - const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz); - console.log(`Saving quiz ${markdownPath}`); - await fs.writeFile(markdownPath, quizMarkdown); - } - } +// if (!previousQuiz) { +// const markdownPath = path.join(quizzesDirectory, `${quiz.name}.md`); +// const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz); +// console.log(`Saving quiz ${markdownPath}`); +// await fs.writeFile(markdownPath, quizMarkdown); +// } +// } - await removeOldQuizzes(quizzesDirectory, module); -}; +// await removeOldQuizzes(quizzesDirectory, module); +// }; const saveAssignments = async ( course: LocalCourse, @@ -232,14 +232,14 @@ const removeOldPages = async (pagesDirectory: string, module: LocalModule) => { } }; -export const courseMarkdownSaver = { - async save(course: LocalCourse, previouslyStoredCourse?: LocalCourse) { - const courseDirectory = path.join(basePath, course.settings.name); - if (!(await directoryExists(courseDirectory))) { - await fs.mkdir(courseDirectory, { recursive: true }); - } +// export const courseMarkdownSaver = { +// async save(course: LocalCourse, previouslyStoredCourse?: LocalCourse) { +// const courseDirectory = path.join(basePath, course.settings.name); +// if (!(await directoryExists(courseDirectory))) { +// await fs.mkdir(courseDirectory, { recursive: true }); +// } - await saveSettings(course, courseDirectory); - await saveModules(course, courseDirectory, previouslyStoredCourse); - }, -}; +// await saveSettings(course, courseDirectory); +// await saveModules(course, courseDirectory, previouslyStoredCourse); +// }, +// };