creating assignments

This commit is contained in:
2024-09-13 21:35:11 -06:00
parent 1442f246b5
commit 4803adf604
13 changed files with 400 additions and 37 deletions

View File

@@ -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,

View File

@@ -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({});
});

View File

@@ -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({});
});

View File

@@ -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({
</div>
<div
className={
`
overflow-hidden
` + (expanded ? " max-h-[30vh]" : " max-h-0")
// transition-all duration-1000 ease-in
` overflow-hidden ` + (expanded ? " max-h-[30vh]" : " max-h-0")
}
style={{
transition: "max-height 1s cubic-bezier(0, 1, 0, 1)",
}}
>
<hr />
<Modal buttonText="New Item">
{({ closeModal }) => (
<div>
<NewItemForm moduleName={moduleName} />
<br />
<button onClick={closeModal}>close</button>
</div>
)}
</Modal>
{moduleItems.map(({ type, item }) => (
<div key={item.name}>{item.name}</div>
))}

View File

@@ -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<LocalAssignmentGroup>();
const { data: settings } = useLocalCourseSettingsQuery();
const createAssignment = useCreateAssignmentMutation();
const createPage = useCreatePageMutation();
const createQuiz = useCreateQuizMutation();
const isPending =
createAssignment.isPending || createPage.isPending || createQuiz.isPending;
return (
<form
className="flex flex-col gap-3"
onSubmit={(e) => {
e.preventDefault();
if (type === "Assignment") {
createAssignment.mutate({
assignment: {
name,
description: "",
dueAt: dateToMarkdownString(new Date()),
submissionTypes: [
AssignmentSubmissionType.ONLINE_TEXT_ENTRY,
AssignmentSubmissionType.ONLINE_UPLOAD,
],
allowedFileUploadExtensions: ["pdf"],
rubric: [],
},
moduleName: moduleName,
assignmentName: name,
});
} else if (type === "Quiz") {
} else if (type === "Page") {
}
}}
>
<div>
<ButtonSelect<"Assignment" | "Quiz" | "Page">
options={["Assignment", "Quiz", "Page"]}
getName={(o) => o?.toString() ?? ""}
setSelectedOption={(t) => setType(t ?? "Assignment")}
selectedOption={type}
/>
</div>
<div>
<TextInput label={type + " Name"} value={name} setValue={setName} />
</div>
<div>
<ButtonSelect
options={settings.assignmentGroups}
getName={(g) => g?.name ?? ""}
setSelectedOption={setAssignmentGroup}
selectedOption={assignmentGroup}
/>
</div>
{settings.assignmentGroups.length === 0 && (
<div>
No assignment groups created, create them in the course settings page
</div>
)}
<button>Create</button>
{isPending && <Spinner />}
</form>
);
}

View File

@@ -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;

View File

@@ -0,0 +1,30 @@
import React from "react";
export default function ButtonSelect<T>({
options,
getName,
setSelectedOption,
selectedOption,
}: {
options: T[];
getName: (value: T | undefined) => string;
setSelectedOption: (value: T | undefined) => void;
selectedOption: T | undefined;
}) {
return (
<div className="flex flex-row gap-3 w-min">
{options.map((o) => (
<button
type="button"
key={getName(o)}
className={
getName(o) === getName(selectedOption) ? "" : "btn-outline"
}
onClick={() => setSelectedOption(o)}
>
{getName(o)}
</button>
))}
</div>
);
}

View File

@@ -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 (
<>
<button onClick={openModal} className={buttonClass}>
{buttonText}
</button>
<div
className={
"fixed inset-0 flex items-center justify-center transition-all duration-400 " +
" bg-black" +
(isOpen ? " bg-opacity-50 z-50 " : " bg-opacity-0 -z-50 ")
}
onClick={closeModal}
>
<div
className={
` bg-slate-800 p-6 rounded-lg shadow-lg w-1/3 ` +
` transition-all duration-400 ` +
` ${isOpen ? "opacity-100" : "opacity-0"}`
}
>
{isOpen && children({ closeModal })}
</div>
</div>
</>
);
}

View File

@@ -16,7 +16,7 @@ export default function TextInput({
{label}
<br />
<input
className="bg-slate-800 rounded-md w-full px-1"
className="bg-slate-800 border border-slate-500 rounded-md w-full px-1"
value={value}
onChange={(e) => setValue(e.target.value)}
/>

View File

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

View File

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

View File

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

View File

@@ -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,