mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-27 07:58:31 -06:00
creating assignments
This commit is contained in:
@@ -28,7 +28,26 @@ export const PUT = async (
|
|||||||
) =>
|
) =>
|
||||||
await withErrorHandling(async () => {
|
await withErrorHandling(async () => {
|
||||||
const assignment = await request.json();
|
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,
|
courseName,
|
||||||
moduleName,
|
moduleName,
|
||||||
assignmentName,
|
assignmentName,
|
||||||
|
|||||||
@@ -27,3 +27,16 @@ export const PUT = async (
|
|||||||
await fileStorageService.pages.updatePage(courseName, moduleName, pageName, page);
|
await fileStorageService.pages.updatePage(courseName, moduleName, pageName, page);
|
||||||
return Response.json({});
|
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({});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -6,22 +6,24 @@ export const GET = async (
|
|||||||
{
|
{
|
||||||
params: { courseName, moduleName, quizName },
|
params: { courseName, moduleName, quizName },
|
||||||
}: { params: { courseName: string; moduleName: string; quizName: string } }
|
}: { params: { courseName: string; moduleName: string; quizName: string } }
|
||||||
) => await withErrorHandling(async () => {
|
) =>
|
||||||
|
await withErrorHandling(async () => {
|
||||||
const quiz = await fileStorageService.quizzes.getQuiz(
|
const quiz = await fileStorageService.quizzes.getQuiz(
|
||||||
courseName,
|
courseName,
|
||||||
moduleName,
|
moduleName,
|
||||||
quizName
|
quizName
|
||||||
);
|
);
|
||||||
return Response.json(quiz);
|
return Response.json(quiz);
|
||||||
})
|
});
|
||||||
|
|
||||||
export const PUT = async (
|
export const PUT = async (
|
||||||
request: Request,
|
request: Request,
|
||||||
{
|
{
|
||||||
params: { courseName, moduleName, quizName },
|
params: { courseName, moduleName, quizName },
|
||||||
}: { params: { courseName: string; moduleName: string; quizName: string } }
|
}: { params: { courseName: string; moduleName: string; quizName: string } }
|
||||||
) => await withErrorHandling(async () => {
|
) =>
|
||||||
const quiz = await request.json()
|
await withErrorHandling(async () => {
|
||||||
|
const quiz = await request.json();
|
||||||
await fileStorageService.quizzes.updateQuiz(
|
await fileStorageService.quizzes.updateQuiz(
|
||||||
courseName,
|
courseName,
|
||||||
moduleName,
|
moduleName,
|
||||||
@@ -29,4 +31,21 @@ export const PUT = async (
|
|||||||
quiz
|
quiz
|
||||||
);
|
);
|
||||||
return Response.json({});
|
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({});
|
||||||
|
});
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
import { IModuleItem } from "@/models/local/IModuleItem";
|
import { IModuleItem } from "@/models/local/IModuleItem";
|
||||||
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
|
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import Modal from "../../../../components/Modal";
|
||||||
|
import NewItemForm from "./NewItemForm";
|
||||||
|
|
||||||
export default function ExpandableModule({
|
export default function ExpandableModule({
|
||||||
moduleName,
|
moduleName,
|
||||||
@@ -72,16 +74,21 @@ export default function ExpandableModule({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`
|
` overflow-hidden ` + (expanded ? " max-h-[30vh]" : " max-h-0")
|
||||||
overflow-hidden
|
|
||||||
` + (expanded ? " max-h-[30vh]" : " max-h-0")
|
|
||||||
// transition-all duration-1000 ease-in
|
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
transition: "max-height 1s cubic-bezier(0, 1, 0, 1)",
|
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 }) => (
|
{moduleItems.map(({ type, item }) => (
|
||||||
<div key={item.name}>{item.name}</div>
|
<div key={item.name}>{item.name}</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
83
nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx
Normal file
83
nextjs/src/app/course/[courseName]/modules/NewItemForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -83,6 +83,11 @@ button,
|
|||||||
@apply bg-red-800 hover:bg-red-900 text-red-100;
|
@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 {
|
select {
|
||||||
@apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm sm:text-sm;
|
@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;
|
@apply focus:outline-none focus:ring-blue-500 focus:border-blue-500;
|
||||||
|
|||||||
30
nextjs/src/components/ButtonSelect.tsx
Normal file
30
nextjs/src/components/ButtonSelect.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
nextjs/src/components/Modal.tsx
Normal file
44
nextjs/src/components/Modal.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export default function TextInput({
|
|||||||
{label}
|
{label}
|
||||||
<br />
|
<br />
|
||||||
<input
|
<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}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -147,12 +147,21 @@ export const fileStorageService = {
|
|||||||
);
|
);
|
||||||
return localAssignmentMarkdown.parseMarkdown(rawFile);
|
return localAssignmentMarkdown.parseMarkdown(rawFile);
|
||||||
},
|
},
|
||||||
async updateAssignment(
|
async updateOrCreateAssignment(
|
||||||
courseName: string,
|
courseName: string,
|
||||||
moduleName: string,
|
moduleName: string,
|
||||||
assignmentName: string,
|
assignmentName: string,
|
||||||
assignment: LocalAssignment
|
assignment: LocalAssignment
|
||||||
) {
|
) {
|
||||||
|
const folder = path.join(
|
||||||
|
basePath,
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
"assignments",
|
||||||
|
);
|
||||||
|
await fs.mkdir(folder, { recursive: true });
|
||||||
|
|
||||||
|
|
||||||
const filePath = path.join(
|
const filePath = path.join(
|
||||||
basePath,
|
basePath,
|
||||||
courseName,
|
courseName,
|
||||||
@@ -201,6 +210,13 @@ export const fileStorageService = {
|
|||||||
quizName: string,
|
quizName: string,
|
||||||
quiz: LocalQuiz
|
quiz: LocalQuiz
|
||||||
) {
|
) {
|
||||||
|
const folder = path.join(
|
||||||
|
basePath,
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
"quizzes",
|
||||||
|
);
|
||||||
|
await fs.mkdir(folder, { recursive: true });
|
||||||
const filePath = path.join(
|
const filePath = path.join(
|
||||||
basePath,
|
basePath,
|
||||||
courseName,
|
courseName,
|
||||||
@@ -248,6 +264,14 @@ export const fileStorageService = {
|
|||||||
pageName: string,
|
pageName: string,
|
||||||
page: LocalCoursePage
|
page: LocalCoursePage
|
||||||
) {
|
) {
|
||||||
|
const folder = path.join(
|
||||||
|
basePath,
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
"pages",
|
||||||
|
);
|
||||||
|
await fs.mkdir(folder, { recursive: true });
|
||||||
|
|
||||||
const filePath = path.join(
|
const filePath = path.join(
|
||||||
basePath,
|
basePath,
|
||||||
courseName,
|
courseName,
|
||||||
|
|||||||
Reference in New Issue
Block a user