mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
Compare commits
12 Commits
9ce42c21f9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d934f27f3 | |||
| e960e2fa42 | |||
|
|
e3079cbc5a | ||
|
|
f5a50fdc02 | ||
| aa191fe90b | |||
| ca240811f2 | |||
| c224a6a9e2 | |||
| 865e86d11f | |||
| b62dcb9c62 | |||
|
|
77fde8198e | ||
|
|
8172724a4f | ||
|
|
07155991aa |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,4 +43,5 @@ next-env.d.ts
|
|||||||
|
|
||||||
|
|
||||||
storage/
|
storage/
|
||||||
temp/
|
temp/
|
||||||
|
.claude/
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
} from "@/features/local/utils/timeUtils";
|
} from "@/features/local/utils/timeUtils";
|
||||||
import { useDraggingContext } from "../../context/drag/draggingContext";
|
import { useDraggingContext } from "../../context/drag/draggingContext";
|
||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
||||||
import { ItemInDay } from "./ItemInDay";
|
import { ItemInDay } from "./itemInDay/ItemInDay";
|
||||||
import { useTodaysItems } from "./useTodaysItems";
|
import { useTodaysItems } from "./useTodaysItems";
|
||||||
import { DayTitle } from "./DayTitle";
|
import { DayTitle } from "./DayTitle";
|
||||||
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
|
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
|
||||||
@@ -13,7 +13,7 @@ import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
|
|||||||
export default function Day({ day, month }: { day: string; month: number }) {
|
export default function Day({ day, month }: { day: string; month: number }) {
|
||||||
const dayAsDate = getDateFromStringOrThrow(
|
const dayAsDate = getDateFromStringOrThrow(
|
||||||
day,
|
day,
|
||||||
"calculating same month in day"
|
"calculating same month in day",
|
||||||
);
|
);
|
||||||
const isToday =
|
const isToday =
|
||||||
getDateOnlyMarkdownString(new Date()) ===
|
getDateOnlyMarkdownString(new Date()) ===
|
||||||
@@ -31,8 +31,8 @@ export default function Day({ day, month }: { day: string; month: number }) {
|
|||||||
(holidaysHappeningToday, holiday) => {
|
(holidaysHappeningToday, holiday) => {
|
||||||
const holidayDates = holiday.days.map((d) =>
|
const holidayDates = holiday.days.map((d) =>
|
||||||
getDateOnlyMarkdownString(
|
getDateOnlyMarkdownString(
|
||||||
getDateFromStringOrThrow(d, "holiday date in day component")
|
getDateFromStringOrThrow(d, "holiday date in day component"),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const today = getDateOnlyMarkdownString(dayAsDate);
|
const today = getDateOnlyMarkdownString(dayAsDate);
|
||||||
|
|
||||||
@@ -40,16 +40,16 @@ export default function Day({ day, month }: { day: string; month: number }) {
|
|||||||
return [...holidaysHappeningToday, holiday.name];
|
return [...holidaysHappeningToday, holiday.name];
|
||||||
return holidaysHappeningToday;
|
return holidaysHappeningToday;
|
||||||
},
|
},
|
||||||
[] as string[]
|
[] as string[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const semesterStart = getDateFromStringOrThrow(
|
const semesterStart = getDateFromStringOrThrow(
|
||||||
settings.startDate,
|
settings.startDate,
|
||||||
"comparing start date in day"
|
"comparing start date in day",
|
||||||
);
|
);
|
||||||
const semesterEnd = getDateFromStringOrThrow(
|
const semesterEnd = getDateFromStringOrThrow(
|
||||||
settings.endDate,
|
settings.endDate,
|
||||||
"comparing end date in day"
|
"comparing end date in day",
|
||||||
);
|
);
|
||||||
|
|
||||||
const isInSemester = semesterStart < dayAsDate && semesterEnd > dayAsDate;
|
const isInSemester = semesterStart < dayAsDate && semesterEnd > dayAsDate;
|
||||||
@@ -90,7 +90,7 @@ export default function Day({ day, month }: { day: string; month: number }) {
|
|||||||
status={status}
|
status={status}
|
||||||
message={message}
|
message={message}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
{todaysQuizzes.map(({ quiz, moduleName, status, message }) => (
|
{todaysQuizzes.map(({ quiz, moduleName, status, message }) => (
|
||||||
<ItemInDay
|
<ItemInDay
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, FC, useState } from "react";
|
||||||
|
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
||||||
|
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
||||||
|
import { useCalendarItemsContext } from "../../../context/calendarItemsContext";
|
||||||
|
import {
|
||||||
|
useCreateAssignmentMutation,
|
||||||
|
useDeleteAssignmentMutation,
|
||||||
|
} from "@/features/local/assignments/assignmentHooks";
|
||||||
|
import {
|
||||||
|
useCanvasAssignmentsQuery,
|
||||||
|
useUpdateAssignmentInCanvasMutation,
|
||||||
|
useDeleteAssignmentFromCanvasMutation,
|
||||||
|
useAddAssignmentToCanvasMutation,
|
||||||
|
canvasAssignmentKeys,
|
||||||
|
} from "@/features/canvas/hooks/canvasAssignmentHooks";
|
||||||
|
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
||||||
|
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
||||||
|
import { useCourseContext } from "../../../context/courseContext";
|
||||||
|
import Modal, { ModalControl } from "@/components/Modal";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
function getDuplicateName(name: string, existingNames: string[]): string {
|
||||||
|
const match = name.match(/^(.*)\s+(\d+)$/);
|
||||||
|
const baseName = match ? match[1] : name;
|
||||||
|
const startNum = match ? parseInt(match[2]) + 1 : 2;
|
||||||
|
let num = startNum;
|
||||||
|
while (existingNames.includes(`${baseName} ${num}`)) {
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
return `${baseName} ${num}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AssignmentDayItemContextMenu: FC<{
|
||||||
|
modalControl: ModalControl;
|
||||||
|
item: IModuleItem;
|
||||||
|
moduleName: string;
|
||||||
|
}> = ({ modalControl, item, moduleName }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { courseName } = useCourseContext();
|
||||||
|
const calendarItems = useCalendarItemsContext();
|
||||||
|
const createAssignmentMutation = useCreateAssignmentMutation();
|
||||||
|
const deleteLocalMutation = useDeleteAssignmentMutation();
|
||||||
|
const updateInCanvasMutation = useUpdateAssignmentInCanvasMutation();
|
||||||
|
const deleteFromCanvasMutation = useDeleteAssignmentFromCanvasMutation();
|
||||||
|
const addToCanvasMutation = useAddAssignmentToCanvasMutation();
|
||||||
|
const { data: canvasAssignments } = useCanvasAssignmentsQuery();
|
||||||
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
|
|
||||||
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||||
|
|
||||||
|
const assignmentInCanvas = canvasAssignments?.find(
|
||||||
|
(a) => a.name === item.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const canvasUrl = assignmentInCanvas
|
||||||
|
? `${baseCanvasUrl}/courses/${settings.canvasId}/assignments/${assignmentInCanvas.id}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setConfirmingDelete(false);
|
||||||
|
modalControl.closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
};
|
||||||
|
}, [modalControl]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
for (let i = 1; i <= 8; i += 2) {
|
||||||
|
setTimeout(() => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: canvasAssignmentKeys.assignments(settings.canvasId),
|
||||||
|
});
|
||||||
|
}, i * 1000);
|
||||||
|
}
|
||||||
|
setConfirmingDelete(false);
|
||||||
|
modalControl.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDuplicate = () => {
|
||||||
|
const assignment = item as LocalAssignment;
|
||||||
|
const existingNames = Object.values(calendarItems).flatMap((modules) =>
|
||||||
|
(modules[moduleName]?.assignments ?? []).map((a) => a.name),
|
||||||
|
);
|
||||||
|
const newName = getDuplicateName(item.name, existingNames);
|
||||||
|
createAssignmentMutation.mutate({
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
assignmentName: newName,
|
||||||
|
assignment: { ...assignment, name: newName },
|
||||||
|
});
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
deleteLocalMutation.mutate({
|
||||||
|
courseName,
|
||||||
|
moduleName,
|
||||||
|
assignmentName: item.name,
|
||||||
|
});
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateCanvas = () => {
|
||||||
|
if (assignmentInCanvas) {
|
||||||
|
updateInCanvasMutation.mutate({
|
||||||
|
canvasAssignmentId: assignmentInCanvas.id,
|
||||||
|
assignment: item as LocalAssignment,
|
||||||
|
});
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteFromCanvas = () => {
|
||||||
|
if (assignmentInCanvas) {
|
||||||
|
deleteFromCanvasMutation.mutate({
|
||||||
|
canvasAssignmentId: assignmentInCanvas.id,
|
||||||
|
assignmentName: item.name,
|
||||||
|
});
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddToCanvas = () => {
|
||||||
|
addToCanvasMutation.mutate({
|
||||||
|
assignment: item as LocalAssignment,
|
||||||
|
moduleName,
|
||||||
|
});
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseButtonClasses = " font-bold text-left py-1";
|
||||||
|
const normalButtonClass =
|
||||||
|
"hover:bg-blue-900 disabled:opacity-50 bg-blue-900/50 text-blue-50 border border-blue-800/70 rounded ";
|
||||||
|
const dangerClasses =
|
||||||
|
"bg-rose-900/30 hover:bg-rose-950 disabled:opacity-50 text-rose-50 border border-rose-900/40 rounded";
|
||||||
|
return (
|
||||||
|
<Modal modalControl={modalControl} backgroundCoverColor="bg-black/30">
|
||||||
|
{() => (
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="text-center p-1 text-slate-200 ">{item.name}</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{confirmingDelete ? (
|
||||||
|
<>
|
||||||
|
<div className={``}>Delete from disk?</div>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className={`unstyled ${baseButtonClasses} ${normalButtonClass}`}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
|
||||||
|
>
|
||||||
|
Yes, delete
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{canvasUrl && (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={canvasUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={` block px-3 ${baseButtonClasses} ${normalButtonClass}`}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
View in Canvas
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={handleUpdateCanvas}
|
||||||
|
disabled={updateInCanvasMutation.isPending}
|
||||||
|
className={`unstyled ${baseButtonClasses} ${normalButtonClass}`}
|
||||||
|
>
|
||||||
|
Update in Canvas
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteFromCanvas}
|
||||||
|
disabled={deleteFromCanvasMutation.isPending}
|
||||||
|
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
|
||||||
|
>
|
||||||
|
Delete from Canvas
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!canvasUrl && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handleAddToCanvas}
|
||||||
|
disabled={addToCanvasMutation.isPending}
|
||||||
|
className={`unstyled ${baseButtonClasses} ${normalButtonClass}`}
|
||||||
|
>
|
||||||
|
Add to Canvas
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setConfirmingDelete(true)}
|
||||||
|
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
|
||||||
|
>
|
||||||
|
Delete from Disk
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleDuplicate}
|
||||||
|
className={`unstyled ${baseButtonClasses} ${normalButtonClass}`}
|
||||||
|
>
|
||||||
|
Duplicate
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"use client";
|
||||||
|
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
||||||
|
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
export const GetPreviewContent: FC<{
|
||||||
|
type: "assignment" | "page" | "quiz";
|
||||||
|
item: IModuleItem;
|
||||||
|
}> = ({ type, item }) => {
|
||||||
|
if (type === "assignment" && "description" in item) {
|
||||||
|
const assignment = item as {
|
||||||
|
description: string;
|
||||||
|
githubClassroomAssignmentShareLink?: string;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<MarkdownDisplay
|
||||||
|
markdown={assignment.description}
|
||||||
|
replaceText={[
|
||||||
|
{
|
||||||
|
source: "insert_github_classroom_url",
|
||||||
|
destination: assignment.githubClassroomAssignmentShareLink || "",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === "page" && "text" in item) {
|
||||||
|
return <MarkdownDisplay markdown={item.text as string} />;
|
||||||
|
} else if (type === "quiz" && "questions" in item) {
|
||||||
|
const quiz = item as { questions: { text: string }[] };
|
||||||
|
return quiz.questions.map((q, i: number) => (
|
||||||
|
<div key={i} className="">
|
||||||
|
<MarkdownDisplay markdown={q.text as string} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,64 +1,36 @@
|
|||||||
|
"use client";
|
||||||
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
||||||
import { getModuleItemUrl } from "@/services/urlUtils";
|
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import { useCourseContext } from "../../context/courseContext";
|
import { useCourseContext } from "../../../context/courseContext";
|
||||||
import { useTooltip } from "@/components/useTooltip";
|
import { useTooltip } from "@/components/useTooltip";
|
||||||
import MarkdownDisplay from "@/components/MarkdownDisplay";
|
import { DraggableItem } from "../../../context/drag/draggingContext";
|
||||||
import { DraggableItem } from "../../context/drag/draggingContext";
|
|
||||||
import ClientOnly from "@/components/ClientOnly";
|
import ClientOnly from "@/components/ClientOnly";
|
||||||
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
|
import { useDragStyleContext } from "../../../context/drag/dragStyleContext";
|
||||||
import { Tooltip } from "../../../../../components/Tooltip";
|
import { Tooltip } from "../../../../../../components/Tooltip";
|
||||||
|
import { AssignmentDayItemContextMenu } from "./DayItemContextMenu";
|
||||||
|
import { GetPreviewContent } from "./GetPreviewContent";
|
||||||
|
import { useModal } from "@/components/Modal";
|
||||||
|
|
||||||
function getPreviewContent(
|
export const ItemInDay: FC<{
|
||||||
type: "assignment" | "page" | "quiz",
|
|
||||||
item: IModuleItem
|
|
||||||
): ReactNode {
|
|
||||||
if (type === "assignment" && "description" in item) {
|
|
||||||
const assignment = item as {
|
|
||||||
description: string;
|
|
||||||
githubClassroomAssignmentShareLink?: string;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<MarkdownDisplay
|
|
||||||
markdown={assignment.description}
|
|
||||||
replaceText={[
|
|
||||||
{
|
|
||||||
source: "insert_github_classroom_url",
|
|
||||||
destination: assignment.githubClassroomAssignmentShareLink || "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (type === "page" && "text" in item) {
|
|
||||||
return <MarkdownDisplay markdown={item.text as string} />;
|
|
||||||
} else if (type === "quiz" && "questions" in item) {
|
|
||||||
const quiz = item as { questions: { text: string }[] };
|
|
||||||
return quiz.questions.map((q, i: number) => (
|
|
||||||
<div key={i} className="">
|
|
||||||
<MarkdownDisplay markdown={q.text as string} />
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ItemInDay({
|
|
||||||
type,
|
|
||||||
moduleName,
|
|
||||||
status,
|
|
||||||
item,
|
|
||||||
message,
|
|
||||||
}: {
|
|
||||||
type: "assignment" | "page" | "quiz";
|
type: "assignment" | "page" | "quiz";
|
||||||
status: "localOnly" | "incomplete" | "published";
|
status: "localOnly" | "incomplete" | "published";
|
||||||
moduleName: string;
|
moduleName: string;
|
||||||
item: IModuleItem;
|
item: IModuleItem;
|
||||||
message: ReactNode;
|
message: ReactNode;
|
||||||
}) {
|
}> = ({ type, moduleName, status, item, message }) => {
|
||||||
const { courseName } = useCourseContext();
|
const { courseName } = useCourseContext();
|
||||||
const { setIsDragging } = useDragStyleContext();
|
const { setIsDragging } = useDragStyleContext();
|
||||||
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
|
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
|
||||||
|
const modalControl = useModal();
|
||||||
|
|
||||||
|
const handleContextMenu = (e: React.MouseEvent) => {
|
||||||
|
if (type !== "assignment") return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
modalControl.openModal({ x: e.clientX, y: e.clientY });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={" relative group "}>
|
<div className={" relative group "}>
|
||||||
@@ -83,31 +55,39 @@ export function ItemInDay({
|
|||||||
};
|
};
|
||||||
e.dataTransfer.setData(
|
e.dataTransfer.setData(
|
||||||
"draggableItem",
|
"draggableItem",
|
||||||
JSON.stringify(draggableItem)
|
JSON.stringify(draggableItem),
|
||||||
);
|
);
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
}}
|
}}
|
||||||
onMouseEnter={showTooltip}
|
onMouseEnter={showTooltip}
|
||||||
onMouseLeave={hideTooltip}
|
onMouseLeave={hideTooltip}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
ref={targetRef}
|
ref={targetRef}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
{status === "published" ? (
|
{status === "published" ? (
|
||||||
getPreviewContent(type, item) && (
|
<Tooltip
|
||||||
<Tooltip
|
message={
|
||||||
message={
|
<div className="max-w-md">
|
||||||
<div className="max-w-md">{getPreviewContent(type, item)}</div>
|
<GetPreviewContent type={type} item={item} />
|
||||||
}
|
</div>
|
||||||
targetRef={targetRef}
|
}
|
||||||
visible={visible}
|
targetRef={targetRef}
|
||||||
/>
|
visible={visible}
|
||||||
)
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip message={message} targetRef={targetRef} visible={visible} />
|
<Tooltip message={message} targetRef={targetRef} visible={visible} />
|
||||||
)}
|
)}
|
||||||
|
{type === "assignment" && (
|
||||||
|
<AssignmentDayItemContextMenu
|
||||||
|
modalControl={modalControl}
|
||||||
|
item={item}
|
||||||
|
moduleName={moduleName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
useAddAssignmentToCanvasMutation,
|
useAddAssignmentToCanvasMutation,
|
||||||
useDeleteAssignmentFromCanvasMutation,
|
useDeleteAssignmentFromCanvasMutation,
|
||||||
useUpdateAssignmentInCanvasMutation,
|
useUpdateAssignmentInCanvasMutation,
|
||||||
|
canvasAssignmentKeys,
|
||||||
} from "@/features/canvas/hooks/canvasAssignmentHooks";
|
} from "@/features/canvas/hooks/canvasAssignmentHooks";
|
||||||
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +20,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useItemNavigation } from "../../../../hooks/useItemNavigation";
|
import { useItemNavigation } from "../../../../hooks/useItemNavigation";
|
||||||
import ItemNavigationButtons from "../../../../components/ItemNavigationButtons";
|
import ItemNavigationButtons from "../../../../components/ItemNavigationButtons";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
export function AssignmentFooterButtons({
|
export function AssignmentFooterButtons({
|
||||||
moduleName,
|
moduleName,
|
||||||
@@ -34,9 +36,10 @@ export function AssignmentFooterButtons({
|
|||||||
const { data: settings } = useLocalCourseSettingsQuery();
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
const { data: canvasAssignments, isFetching: canvasIsFetching } =
|
const { data: canvasAssignments, isFetching: canvasIsFetching } =
|
||||||
useCanvasAssignmentsQuery();
|
useCanvasAssignmentsQuery();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { data: assignment, isFetching } = useAssignmentQuery(
|
const { data: assignment, isFetching } = useAssignmentQuery(
|
||||||
moduleName,
|
moduleName,
|
||||||
assignmentName
|
assignmentName,
|
||||||
);
|
);
|
||||||
const addToCanvas = useAddAssignmentToCanvasMutation();
|
const addToCanvas = useAddAssignmentToCanvasMutation();
|
||||||
const deleteFromCanvas = useDeleteAssignmentFromCanvasMutation();
|
const deleteFromCanvas = useDeleteAssignmentFromCanvasMutation();
|
||||||
@@ -47,11 +50,11 @@ export function AssignmentFooterButtons({
|
|||||||
const { previousUrl, nextUrl } = useItemNavigation(
|
const { previousUrl, nextUrl } = useItemNavigation(
|
||||||
"assignment",
|
"assignment",
|
||||||
assignmentName,
|
assignmentName,
|
||||||
moduleName
|
moduleName,
|
||||||
);
|
);
|
||||||
|
|
||||||
const assignmentInCanvas = canvasAssignments?.find(
|
const assignmentInCanvas = canvasAssignments?.find(
|
||||||
(a) => a.name === assignmentName
|
(a) => a.name === assignmentName,
|
||||||
);
|
);
|
||||||
|
|
||||||
const anythingIsLoading =
|
const anythingIsLoading =
|
||||||
@@ -84,6 +87,17 @@ export function AssignmentFooterButtons({
|
|||||||
className="btn"
|
className="btn"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${baseCanvasUrl}/courses/${settings.canvasId}/assignments/${assignmentInCanvas.id}`}
|
href={`${baseCanvasUrl}/courses/${settings.canvasId}/assignments/${assignmentInCanvas.id}`}
|
||||||
|
onClick={() => {
|
||||||
|
for (let i = 1; i <= 8; i += 2) {
|
||||||
|
setTimeout(() => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: canvasAssignmentKeys.assignments(
|
||||||
|
settings.canvasId,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}, i * 1000);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
View in Canvas
|
View in Canvas
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -75,15 +75,14 @@ export default function AssignmentPreview({
|
|||||||
{extraPoints !== 0 && (
|
{extraPoints !== 0 && (
|
||||||
<h5 className="text-center">{extraPoints} Extra Credit Points</h5>
|
<h5 className="text-center">{extraPoints} Extra Credit Points</h5>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-3">
|
<div className="grid grid-cols-[auto_auto_1fr]">
|
||||||
{assignment.rubric.map((rubricItem, i) => (
|
{assignment.rubric.map((rubricItem, i) => (
|
||||||
<Fragment key={rubricItem.label + i}>
|
<Fragment key={rubricItem.label + i}>
|
||||||
<div className="text-end pe-3 col-span-2">{rubricItem.label}</div>
|
<div className="text-end pe-1">
|
||||||
<div>
|
{rubricItemIsExtraCredit(rubricItem) ? "Extra Credit" : ""}
|
||||||
{rubricItem.points}
|
|
||||||
|
|
||||||
{rubricItemIsExtraCredit(rubricItem) ? " - Extra Credit" : ""}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-end pe-3">{rubricItem.points}</div>
|
||||||
|
<div>{rubricItem.label}</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default function EditQuiz({
|
|||||||
const updateQuizMutation = useUpdateQuizMutation();
|
const updateQuizMutation = useUpdateQuizMutation();
|
||||||
const { data: globalSettings } = useGlobalSettingsQuery();
|
const { data: globalSettings } = useGlobalSettingsQuery();
|
||||||
const feedbackDelimiters = getFeedbackDelimitersFromSettings(
|
const feedbackDelimiters = getFeedbackDelimitersFromSettings(
|
||||||
(globalSettings ?? ({} as GlobalSettings)) as GlobalSettings
|
(globalSettings ?? ({} as GlobalSettings)) as GlobalSettings,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { clientIsAuthoritative, text, textUpdate, monacoKey } =
|
const { clientIsAuthoritative, text, textUpdate, monacoKey } =
|
||||||
@@ -141,14 +141,14 @@ export default function EditQuiz({
|
|||||||
quizMarkdownUtils.toMarkdown(quiz, feedbackDelimiters) !==
|
quizMarkdownUtils.toMarkdown(quiz, feedbackDelimiters) !==
|
||||||
quizMarkdownUtils.toMarkdown(
|
quizMarkdownUtils.toMarkdown(
|
||||||
quizMarkdownUtils.parseMarkdown(text, name, feedbackDelimiters),
|
quizMarkdownUtils.parseMarkdown(text, name, feedbackDelimiters),
|
||||||
feedbackDelimiters
|
feedbackDelimiters,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (clientIsAuthoritative) {
|
if (clientIsAuthoritative) {
|
||||||
const updatedQuiz = quizMarkdownUtils.parseMarkdown(
|
const updatedQuiz = quizMarkdownUtils.parseMarkdown(
|
||||||
text,
|
text,
|
||||||
quizName,
|
quizName,
|
||||||
feedbackDelimiters
|
feedbackDelimiters,
|
||||||
);
|
);
|
||||||
await updateQuizMutation.mutateAsync({
|
await updateQuizMutation.mutateAsync({
|
||||||
quiz: updatedQuiz,
|
quiz: updatedQuiz,
|
||||||
@@ -160,7 +160,7 @@ export default function EditQuiz({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"client not authoritative, updating client with server quiz"
|
"client not authoritative, updating client with server quiz",
|
||||||
);
|
);
|
||||||
textUpdate(quizMarkdownUtils.toMarkdown(quiz), true);
|
textUpdate(quizMarkdownUtils.toMarkdown(quiz), true);
|
||||||
}
|
}
|
||||||
@@ -178,6 +178,7 @@ export default function EditQuiz({
|
|||||||
}, [
|
}, [
|
||||||
clientIsAuthoritative,
|
clientIsAuthoritative,
|
||||||
courseName,
|
courseName,
|
||||||
|
feedbackDelimiters,
|
||||||
isFetching,
|
isFetching,
|
||||||
moduleName,
|
moduleName,
|
||||||
quiz,
|
quiz,
|
||||||
|
|||||||
@@ -3,23 +3,35 @@ import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
|||||||
|
|
||||||
export interface ModalControl {
|
export interface ModalControl {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
openModal: () => void;
|
openModal: (position?: { x: number; y: number }) => void;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
|
position?: { x: number; y: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModal() {
|
export function useModal() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [position, setPosition] = useState<
|
||||||
|
{ x: number; y: number } | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const openModal = useCallback(() => setIsOpen(true), []);
|
const openModal = useCallback((pos?: { x: number; y: number }) => {
|
||||||
const closeModal = useCallback(() => setIsOpen(false), []);
|
setPosition(pos);
|
||||||
|
setIsOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeModal = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setPosition(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
isOpen,
|
isOpen,
|
||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
position,
|
||||||
}),
|
}),
|
||||||
[closeModal, isOpen, openModal]
|
[closeModal, isOpen, openModal, position],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +42,7 @@ export default function Modal({
|
|||||||
modalWidth = "w-1/3",
|
modalWidth = "w-1/3",
|
||||||
modalControl,
|
modalControl,
|
||||||
buttonComponent,
|
buttonComponent,
|
||||||
|
backgroundCoverColor = "bg-black/80",
|
||||||
}: {
|
}: {
|
||||||
children: (props: { closeModal: () => void }) => ReactNode;
|
children: (props: { closeModal: () => void }) => ReactNode;
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
@@ -37,21 +50,25 @@ export default function Modal({
|
|||||||
modalWidth?: string;
|
modalWidth?: string;
|
||||||
modalControl: ModalControl;
|
modalControl: ModalControl;
|
||||||
buttonComponent?: (props: { openModal: () => void }) => ReactNode;
|
buttonComponent?: (props: { openModal: () => void }) => ReactNode;
|
||||||
|
backgroundCoverColor?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{buttonComponent ? (
|
{buttonComponent
|
||||||
buttonComponent({ openModal: modalControl.openModal })
|
? buttonComponent({ openModal: () => modalControl.openModal() })
|
||||||
) : (
|
: buttonText && (
|
||||||
<button onClick={modalControl.openModal} className={buttonClass}>
|
<button
|
||||||
{buttonText}
|
onClick={() => modalControl.openModal()}
|
||||||
</button>
|
className={buttonClass}
|
||||||
)}
|
>
|
||||||
|
{buttonText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
modalControl.isOpen
|
modalControl.isOpen
|
||||||
? "transition-all duration-400 fixed inset-0 flex items-center justify-center h-screen bg-black/80 z-50 w-screen"
|
? `transition-all duration-400 fixed inset-0 ${modalControl.position ? "" : "flex items-center justify-center"} h-screen ${backgroundCoverColor} z-50 w-screen`
|
||||||
: "hidden h-0 w-0 p-1 -z-50"
|
: "hidden h-0 w-0 p-1 -z-50"
|
||||||
}
|
}
|
||||||
onClick={modalControl.closeModal}
|
onClick={modalControl.closeModal}
|
||||||
@@ -60,11 +77,15 @@ export default function Modal({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className={
|
className={`bg-slate-800 ${modalControl.position ? "" : "p-6"} rounded-lg shadow-lg ${modalControl.position ? "" : modalWidth} transition-all duration-400 ${modalControl.isOpen ? "opacity-100" : "opacity-0"}`}
|
||||||
` bg-slate-800 p-6 rounded-lg shadow-lg ` +
|
style={
|
||||||
modalWidth +
|
modalControl.position
|
||||||
` transition-all duration-400 ` +
|
? {
|
||||||
` ${modalControl.isOpen ? "opacity-100" : "opacity-0"}`
|
position: "fixed",
|
||||||
|
left: modalControl.position.x,
|
||||||
|
top: modalControl.position.y,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{modalControl.isOpen &&
|
{modalControl.isOpen &&
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export async function getModuleNamesFromFiles(courseName: string) {
|
|||||||
.map((dirent) => dirent.name);
|
.map((dirent) => dirent.name);
|
||||||
|
|
||||||
const modules = await Promise.all(modulePromises);
|
const modules = await Promise.all(modulePromises);
|
||||||
const modulesWithoutLectures = modules.filter((m) => m !== lectureFolderName);
|
const modulesWithoutLectures = modules.filter(
|
||||||
|
(m) => m !== lectureFolderName && !m.startsWith(".")
|
||||||
|
);
|
||||||
return modulesWithoutLectures.sort((a, b) => a.localeCompare(b));
|
return modulesWithoutLectures.sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user