diff --git a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/[assignmentName]/route.ts b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/[assignmentName]/route.ts
index 7c76946..6e3cd08 100644
--- a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/[assignmentName]/route.ts
+++ b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/assignments/[assignmentName]/route.ts
@@ -28,7 +28,26 @@ export const PUT = async (
) =>
await withErrorHandling(async () => {
const assignment = await request.json();
- await fileStorageService.assignments.updateAssignment(
+ await fileStorageService.assignments.updateOrCreateAssignment(
+ courseName,
+ moduleName,
+ assignmentName,
+ assignment
+ );
+ return Response.json({});
+ });
+
+export const POST = async (
+ request: Request,
+ {
+ params: { courseName, moduleName, assignmentName },
+ }: {
+ params: { courseName: string; moduleName: string; assignmentName: string };
+ }
+) =>
+ await withErrorHandling(async () => {
+ const assignment = await request.json();
+ await fileStorageService.assignments.updateOrCreateAssignment(
courseName,
moduleName,
assignmentName,
diff --git a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/pages/[pageName]/route.ts b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/pages/[pageName]/route.ts
index 094e28c..5f5eb3a 100644
--- a/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/pages/[pageName]/route.ts
+++ b/nextjs/src/app/api/courses/[courseName]/modules/[moduleName]/pages/[pageName]/route.ts
@@ -16,14 +16,27 @@ export const GET = async (
return Response.json(settings);
});
-export const PUT = async (
- request: Request,
- {
- params: { courseName, moduleName, pageName },
- }: { params: { courseName: string; moduleName: string; pageName: string } }
-) =>
- await withErrorHandling(async () => {
- const page = await request.json();
- await fileStorageService.pages.updatePage(courseName, moduleName, pageName, page);
- return Response.json({});
- });
+ export const PUT = async (
+ request: Request,
+ {
+ params: { courseName, moduleName, pageName },
+ }: { params: { courseName: string; moduleName: string; pageName: string } }
+ ) =>
+ await withErrorHandling(async () => {
+ const page = await request.json();
+ await fileStorageService.pages.updatePage(courseName, moduleName, pageName, page);
+ return Response.json({});
+ });
+
+ export const POST = async (
+ request: Request,
+ {
+ params: { courseName, moduleName, pageName },
+ }: { params: { courseName: string; moduleName: string; pageName: string } }
+ ) =>
+ await withErrorHandling(async () => {
+ const page = await request.json();
+ await fileStorageService.pages.updatePage(courseName, moduleName, pageName, page);
+ return Response.json({});
+ });
+
\ No newline at end of file
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 f7afbfc..25da7fc 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,27 +6,46 @@ export const GET = async (
{
params: { courseName, moduleName, quizName },
}: { params: { courseName: string; moduleName: string; quizName: string } }
-) => await withErrorHandling(async () => {
- const quiz = await fileStorageService.quizzes.getQuiz(
- courseName,
- moduleName,
- quizName
- );
- return Response.json(quiz);
-})
+) =>
+ await withErrorHandling(async () => {
+ const quiz = await fileStorageService.quizzes.getQuiz(
+ courseName,
+ moduleName,
+ quizName
+ );
+ return Response.json(quiz);
+ });
export const PUT = async (
request: Request,
{
params: { courseName, moduleName, quizName },
}: { params: { courseName: string; moduleName: string; quizName: string } }
-) => await withErrorHandling(async () => {
- const quiz = await request.json()
- await fileStorageService.quizzes.updateQuiz(
- courseName,
- moduleName,
- quizName,
- quiz
- );
- return Response.json({});
-})
+) =>
+ await withErrorHandling(async () => {
+ const quiz = await request.json();
+ await fileStorageService.quizzes.updateQuiz(
+ courseName,
+ moduleName,
+ quizName,
+ quiz
+ );
+ return Response.json({});
+ });
+
+export const POST = async (
+ request: Request,
+ {
+ params: { courseName, moduleName, quizName },
+ }: { params: { courseName: string; moduleName: string; quizName: string } }
+) =>
+ await withErrorHandling(async () => {
+ const quiz = await request.json();
+ await fileStorageService.quizzes.updateQuiz(
+ courseName,
+ moduleName,
+ quizName,
+ quiz
+ );
+ return Response.json({});
+ });
diff --git a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx
index b0222eb..bcf97a8 100644
--- a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx
+++ b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx
@@ -13,6 +13,8 @@ import {
import { IModuleItem } from "@/models/local/IModuleItem";
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
import { useState } from "react";
+import Modal from "../../../../components/Modal";
+import NewItemForm from "./NewItemForm";
export default function ExpandableModule({
moduleName,
@@ -72,16 +74,21 @@ export default function ExpandableModule({
-
+
+ {({ closeModal }) => (
+
+
+
+
+
+ )}
+
{moduleItems.map(({ type, item }) => (
{item.name}
))}
diff --git a/nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx b/nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx
new file mode 100644
index 0000000..f8f370f
--- /dev/null
+++ b/nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx
@@ -0,0 +1,83 @@
+"use client";
+import ButtonSelect from "@/components/ButtonSelect";
+import TextInput from "@/components/form/TextInput";
+import { Spinner } from "@/components/Spinner";
+import { useCreateAssignmentMutation } from "@/hooks/localCourse/assignmentHooks";
+import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
+import { useCreatePageMutation } from "@/hooks/localCourse/pageHooks";
+import { useCreateQuizMutation } from "@/hooks/localCourse/quizHooks";
+import { AssignmentSubmissionType } from "@/models/local/assignment/assignmentSubmissionType";
+import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
+import { dateToMarkdownString } from "@/models/local/timeUtils";
+import React, { useState } from "react";
+
+export default function NewItemForm({ moduleName }: { moduleName: string }) {
+ const [type, setType] = useState<"Assignment" | "Quiz" | "Page">(
+ "Assignment"
+ );
+ const [name, setName] = useState("");
+ const [assignmentGroup, setAssignmentGroup] =
+ useState
();
+ const { data: settings } = useLocalCourseSettingsQuery();
+ const createAssignment = useCreateAssignmentMutation();
+ const createPage = useCreatePageMutation();
+ const createQuiz = useCreateQuizMutation();
+
+ const isPending =
+ createAssignment.isPending || createPage.isPending || createQuiz.isPending;
+
+ return (
+
+ );
+}
diff --git a/nextjs/src/app/globals.css b/nextjs/src/app/globals.css
index 4f80813..deaeccc 100644
--- a/nextjs/src/app/globals.css
+++ b/nextjs/src/app/globals.css
@@ -83,6 +83,11 @@ button,
@apply bg-red-800 hover:bg-red-900 text-red-100;
}
+.btn-outline {
+ @apply bg-transparent text-blue-200 border border-blue-500;
+ @apply hover:border-blue-500 hover:text-blue-100 hover:bg-blue-900;
+}
+
select {
@apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm sm:text-sm;
@apply focus:outline-none focus:ring-blue-500 focus:border-blue-500;
diff --git a/nextjs/src/components/ButtonSelect.tsx b/nextjs/src/components/ButtonSelect.tsx
new file mode 100644
index 0000000..3ebe7fd
--- /dev/null
+++ b/nextjs/src/components/ButtonSelect.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+
+export default function ButtonSelect({
+ options,
+ getName,
+ setSelectedOption,
+ selectedOption,
+}: {
+ options: T[];
+ getName: (value: T | undefined) => string;
+ setSelectedOption: (value: T | undefined) => void;
+ selectedOption: T | undefined;
+}) {
+ return (
+
+ {options.map((o) => (
+
+ ))}
+
+ );
+}
diff --git a/nextjs/src/components/Modal.tsx b/nextjs/src/components/Modal.tsx
new file mode 100644
index 0000000..9c8086d
--- /dev/null
+++ b/nextjs/src/components/Modal.tsx
@@ -0,0 +1,44 @@
+"use client";
+import React, { ReactNode, useState } from "react";
+
+export default function Modal({
+ children,
+ buttonText,
+ buttonClass = "",
+}: {
+ children: (props: { closeModal: () => void }) => ReactNode;
+ buttonText: string;
+ buttonClass?: string;
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const openModal = () => setIsOpen(true);
+ const closeModal = () => setIsOpen(false);
+
+ return (
+ <>
+
+
+
+
+ {isOpen && children({ closeModal })}
+
+
+ >
+ );
+}
diff --git a/nextjs/src/components/form/TextInput.tsx b/nextjs/src/components/form/TextInput.tsx
index bfb2494..ad471bd 100644
--- a/nextjs/src/components/form/TextInput.tsx
+++ b/nextjs/src/components/form/TextInput.tsx
@@ -16,7 +16,7 @@ export default function TextInput({
{label}
setValue(e.target.value)}
/>
diff --git a/nextjs/src/hooks/localCourse/assignmentHooks.ts b/nextjs/src/hooks/localCourse/assignmentHooks.ts
index b4a8cab..d70ca54 100644
--- a/nextjs/src/hooks/localCourse/assignmentHooks.ts
+++ b/nextjs/src/hooks/localCourse/assignmentHooks.ts
@@ -127,3 +127,46 @@ export const useUpdateAssignmentMutation = () => {
},
});
};
+
+
+export const useCreateAssignmentMutation = () => {
+ const { courseName } = useCourseContext();
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ assignment,
+ moduleName,
+ assignmentName,
+ }: {
+ assignment: LocalAssignment;
+ moduleName: string;
+ assignmentName: string;
+ }) => {
+ queryClient.setQueryData(
+ localCourseKeys.assignment(courseName, moduleName, assignmentName),
+ assignment
+ );
+ const url =
+ "/api/courses/" +
+ encodeURIComponent(courseName) +
+ "/modules/" +
+ encodeURIComponent(moduleName) +
+ "/assignments/" +
+ encodeURIComponent(assignmentName);
+ await axiosClient.post(url, assignment);
+ },
+ onSuccess: (_, { moduleName, assignmentName }) => {
+ queryClient.invalidateQueries({
+ queryKey: localCourseKeys.assignment(
+ courseName,
+ moduleName,
+ assignmentName
+ ),
+ });
+ queryClient.invalidateQueries({
+ queryKey: localCourseKeys.assignmentNames(courseName, moduleName),
+ });
+ },
+ });
+};
+
diff --git a/nextjs/src/hooks/localCourse/pageHooks.ts b/nextjs/src/hooks/localCourse/pageHooks.ts
index f74a119..805b2cc 100644
--- a/nextjs/src/hooks/localCourse/pageHooks.ts
+++ b/nextjs/src/hooks/localCourse/pageHooks.ts
@@ -116,3 +116,41 @@ export const useUpdatePageMutation = () => {
},
});
};
+
+
+export const useCreatePageMutation = () => {
+ const { courseName } = useCourseContext();
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ page,
+ moduleName,
+ pageName,
+ }: {
+ page: LocalCoursePage;
+ moduleName: string;
+ pageName: string;
+ }) => {
+ queryClient.setQueryData(
+ localCourseKeys.page(courseName, moduleName, pageName),
+ page
+ );
+ const url =
+ "/api/courses/" +
+ encodeURIComponent(courseName) +
+ "/modules/" +
+ encodeURIComponent(moduleName) +
+ "/pages/" +
+ encodeURIComponent(pageName);
+ await axiosClient.post(url, page);
+ },
+ onSuccess: (_, { moduleName, pageName }) => {
+ queryClient.invalidateQueries({
+ queryKey: localCourseKeys.page(courseName, moduleName, pageName),
+ });
+ queryClient.invalidateQueries({
+ queryKey: localCourseKeys.pageNames(courseName, moduleName),
+ });
+ },
+ });
+};
diff --git a/nextjs/src/hooks/localCourse/quizHooks.ts b/nextjs/src/hooks/localCourse/quizHooks.ts
index 1029e39..73cb88b 100644
--- a/nextjs/src/hooks/localCourse/quizHooks.ts
+++ b/nextjs/src/hooks/localCourse/quizHooks.ts
@@ -105,3 +105,41 @@ export const useUpdateQuizMutation = () => {
},
});
};
+
+
+export const useCreateQuizMutation = () => {
+ const { courseName } = useCourseContext();
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ quiz,
+ moduleName,
+ quizName,
+ }: {
+ quiz: LocalQuiz;
+ moduleName: string;
+ quizName: string;
+ }) => {
+ queryClient.setQueryData(
+ localCourseKeys.quiz(courseName, moduleName, quizName),
+ quiz
+ );
+ const url =
+ "/api/courses/" +
+ encodeURIComponent(courseName) +
+ "/modules/" +
+ encodeURIComponent(moduleName) +
+ "/quizzes/" +
+ encodeURIComponent(quizName);
+ await axiosClient.post(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 700ca6a..384884f 100644
--- a/nextjs/src/services/fileStorage/fileStorageService.ts
+++ b/nextjs/src/services/fileStorage/fileStorageService.ts
@@ -147,12 +147,21 @@ export const fileStorageService = {
);
return localAssignmentMarkdown.parseMarkdown(rawFile);
},
- async updateAssignment(
+ async updateOrCreateAssignment(
courseName: string,
moduleName: string,
assignmentName: string,
assignment: LocalAssignment
) {
+ const folder = path.join(
+ basePath,
+ courseName,
+ moduleName,
+ "assignments",
+ );
+ await fs.mkdir(folder, { recursive: true });
+
+
const filePath = path.join(
basePath,
courseName,
@@ -201,6 +210,13 @@ export const fileStorageService = {
quizName: string,
quiz: LocalQuiz
) {
+ const folder = path.join(
+ basePath,
+ courseName,
+ moduleName,
+ "quizzes",
+ );
+ await fs.mkdir(folder, { recursive: true });
const filePath = path.join(
basePath,
courseName,
@@ -248,6 +264,14 @@ export const fileStorageService = {
pageName: string,
page: LocalCoursePage
) {
+ const folder = path.join(
+ basePath,
+ courseName,
+ moduleName,
+ "pages",
+ );
+ await fs.mkdir(folder, { recursive: true });
+
const filePath = path.join(
basePath,
courseName,