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);
+// },
+// };