diff --git a/src/app/course/[courseName]/calendar/day/Day.tsx b/src/app/course/[courseName]/calendar/day/Day.tsx
index 5d5f0ec..301a1aa 100644
--- a/src/app/course/[courseName]/calendar/day/Day.tsx
+++ b/src/app/course/[courseName]/calendar/day/Day.tsx
@@ -5,7 +5,7 @@ import {
} from "@/features/local/utils/timeUtils";
import { useDraggingContext } from "../../context/drag/draggingContext";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
-import { ItemInDay } from "./ItemInDay";
+import { ItemInDay } from "./itemInDay/ItemInDay";
import { useTodaysItems } from "./useTodaysItems";
import { DayTitle } from "./DayTitle";
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 }) {
const dayAsDate = getDateFromStringOrThrow(
day,
- "calculating same month in day"
+ "calculating same month in day",
);
const isToday =
getDateOnlyMarkdownString(new Date()) ===
@@ -31,8 +31,8 @@ export default function Day({ day, month }: { day: string; month: number }) {
(holidaysHappeningToday, holiday) => {
const holidayDates = holiday.days.map((d) =>
getDateOnlyMarkdownString(
- getDateFromStringOrThrow(d, "holiday date in day component")
- )
+ getDateFromStringOrThrow(d, "holiday date in day component"),
+ ),
);
const today = getDateOnlyMarkdownString(dayAsDate);
@@ -40,16 +40,16 @@ export default function Day({ day, month }: { day: string; month: number }) {
return [...holidaysHappeningToday, holiday.name];
return holidaysHappeningToday;
},
- [] as string[]
+ [] as string[],
);
const semesterStart = getDateFromStringOrThrow(
settings.startDate,
- "comparing start date in day"
+ "comparing start date in day",
);
const semesterEnd = getDateFromStringOrThrow(
settings.endDate,
- "comparing end date in day"
+ "comparing end date in day",
);
const isInSemester = semesterStart < dayAsDate && semesterEnd > dayAsDate;
@@ -90,7 +90,7 @@ export default function Day({ day, month }: { day: string; month: number }) {
status={status}
message={message}
/>
- )
+ ),
)}
{todaysQuizzes.map(({ quiz, moduleName, status, message }) => (
- );
- } else if (type === "page" && "text" in item) {
- return ;
- } else if (type === "quiz" && "questions" in item) {
- const quiz = item as { questions: { text: string }[] };
- return quiz.questions.map((q, i: number) => (
-
-
-
- ));
- }
- return null;
-}
-
-export function ItemInDay({
- type,
- moduleName,
- status,
- item,
- message,
-}: {
- type: "assignment" | "page" | "quiz";
- status: "localOnly" | "incomplete" | "published";
- moduleName: string;
- item: IModuleItem;
- message: ReactNode;
-}) {
- const { courseName } = useCourseContext();
- const { setIsDragging } = useDragStyleContext();
- const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
-
- const [contextMenuPos, setContextMenuPos] = useState<{
- x: number;
- y: number;
- } | null>(null);
- const [confirmingDelete, setConfirmingDelete] = useState(false);
-
- const { data: canvasAssignments } = useCanvasAssignmentsQuery();
- const { data: settings } = useLocalCourseSettingsQuery();
- const updateInCanvas = useUpdateAssignmentInCanvasMutation();
- const deleteFromCanvas = useDeleteAssignmentFromCanvasMutation();
- const deleteLocal = useDeleteAssignmentMutation();
- const createAssignment = useCreateAssignmentMutation();
- const calendarItems = useCalendarItemsContext();
-
- const assignmentInCanvas =
- type === "assignment"
- ? canvasAssignments?.find((a) => a.name === item.name)
- : undefined;
-
- const handleContextMenu = (e: React.MouseEvent) => {
- if (type !== "assignment") return;
- e.preventDefault();
- e.stopPropagation();
- setContextMenuPos({ x: e.clientX, y: e.clientY });
- setConfirmingDelete(false);
- };
-
- const closeContextMenu = useCallback(() => {
- setContextMenuPos(null);
- setConfirmingDelete(false);
- }, []);
-
- const handleDuplicate = useCallback(() => {
- 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);
- createAssignment.mutate({
- courseName,
- moduleName,
- assignmentName: newName,
- assignment: { ...assignment, name: newName },
- });
- closeContextMenu();
- }, [
- item,
- calendarItems,
- moduleName,
- createAssignment,
- courseName,
- closeContextMenu,
- ]);
-
- const contextMenuItems: ContextMenuItem[] = confirmingDelete
- ? [
- { label: "Delete from disk?", disabled: true },
- {
- label: "Yes, delete",
- variant: "danger",
- onClick: () => {
- deleteLocal.mutate({ courseName, moduleName, assignmentName: item.name });
- closeContextMenu();
- },
- },
- { label: "Cancel", onClick: closeContextMenu },
- ]
- : [
- ...(assignmentInCanvas
- ? [
- {
- label: "View in Canvas",
- href: `${baseCanvasUrl}/courses/${settings.canvasId}/assignments/${assignmentInCanvas.id}`,
- },
- {
- label: "Update in Canvas",
- onClick: () => {
- updateInCanvas.mutate({
- canvasAssignmentId: assignmentInCanvas.id,
- assignment: item as LocalAssignment,
- });
- closeContextMenu();
- },
- disabled: updateInCanvas.isPending,
- },
- {
- label: "Delete from Canvas",
- variant: "danger" as const,
- onClick: () => {
- deleteFromCanvas.mutate({
- canvasAssignmentId: assignmentInCanvas.id,
- assignmentName: item.name,
- });
- closeContextMenu();
- },
- disabled: deleteFromCanvas.isPending,
- },
- ]
- : [
- {
- label: "Delete from Disk",
- variant: "danger" as const,
- onClick: () => setConfirmingDelete(true),
- },
- ]),
- { label: "Duplicate", onClick: handleDuplicate },
- ];
-
- return (
-
- {
- const draggableItem: DraggableItem = {
- type,
- item,
- sourceModuleName: moduleName,
- };
- e.dataTransfer.setData(
- "draggableItem",
- JSON.stringify(draggableItem)
- );
- setIsDragging(true);
- }}
- onMouseEnter={showTooltip}
- onMouseLeave={hideTooltip}
- onContextMenu={handleContextMenu}
- ref={targetRef}
- >
- {item.name}
-
-
- {status === "published" ? (
- getPreviewContent(type, item) && (
- {getPreviewContent(type, item)}
- }
- targetRef={targetRef}
- visible={visible}
- />
- )
- ) : (
-
- )}
- {contextMenuPos && type === "assignment" && (
-
- )}
-
-
- );
-}
diff --git a/src/app/course/[courseName]/calendar/day/itemInDay/DayItemContextMenu.tsx b/src/app/course/[courseName]/calendar/day/itemInDay/DayItemContextMenu.tsx
new file mode 100644
index 0000000..4738092
--- /dev/null
+++ b/src/app/course/[courseName]/calendar/day/itemInDay/DayItemContextMenu.tsx
@@ -0,0 +1,204 @@
+"use client";
+import { useRef, 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,
+} from "@/features/canvas/hooks/canvasAssignmentHooks";
+import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
+import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
+import { useCourseContext } from "../../../context/courseContext";
+
+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 DayItemContextMenu: FC<{
+ x: number;
+ y: number;
+ onClose: () => void;
+ item: IModuleItem;
+ moduleName: string;
+}> = ({ x, y, onClose, item, moduleName }) => {
+ const { courseName } = useCourseContext();
+ const ref = useRef(null);
+ const calendarItems = useCalendarItemsContext();
+ const createAssignment = useCreateAssignmentMutation();
+ const deleteLocal = useDeleteAssignmentMutation();
+ const { data: canvasAssignments } = useCanvasAssignmentsQuery();
+ const { data: settings } = useLocalCourseSettingsQuery();
+ const updateInCanvas = useUpdateAssignmentInCanvasMutation();
+ const deleteFromCanvas = useDeleteAssignmentFromCanvasMutation();
+
+ 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 handleClickOutside = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) {
+ setConfirmingDelete(false);
+ onClose();
+ }
+ };
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ setConfirmingDelete(false);
+ onClose();
+ }
+ };
+ document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("keydown", handleEscape);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("keydown", handleEscape);
+ };
+ }, [onClose]);
+
+ const handleClose = () => {
+ setConfirmingDelete(false);
+ onClose();
+ };
+
+ 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);
+ createAssignment.mutate({
+ courseName,
+ moduleName,
+ assignmentName: newName,
+ assignment: { ...assignment, name: newName },
+ });
+ handleClose();
+ };
+
+ const handleDelete = () => {
+ deleteLocal.mutate({ courseName, moduleName, assignmentName: item.name });
+ handleClose();
+ };
+
+ const handleUpdateCanvas = () => {
+ if (assignmentInCanvas) {
+ updateInCanvas.mutate({
+ canvasAssignmentId: assignmentInCanvas.id,
+ assignment: item as LocalAssignment,
+ });
+ handleClose();
+ }
+ };
+
+ const handleDeleteFromCanvas = () => {
+ if (assignmentInCanvas) {
+ deleteFromCanvas.mutate({
+ canvasAssignmentId: assignmentInCanvas.id,
+ assignmentName: item.name,
+ });
+ handleClose();
+ }
+ };
+
+ const baseButtonClasses = "unstyled w-full text-left px-4 py-2";
+ const normalHoverClasses = "hover:bg-slate-700 disabled:opacity-50";
+ const dangerClasses =
+ "bg-rose-900/30 hover:bg-rose-950 disabled:opacity-50 text-rose-50";
+ return (
+
+ {confirmingDelete ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+ {canvasUrl && (
+ <>
+
+ View in Canvas
+
+
+
+ >
+ )}
+ {!canvasUrl && (
+
+ )}
+
+ >
+ )}
+
+ );
+};
diff --git a/src/app/course/[courseName]/calendar/day/itemInDay/GetPreviewContent.tsx b/src/app/course/[courseName]/calendar/day/itemInDay/GetPreviewContent.tsx
new file mode 100644
index 0000000..117760c
--- /dev/null
+++ b/src/app/course/[courseName]/calendar/day/itemInDay/GetPreviewContent.tsx
@@ -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 (
+
+ );
+ } else if (type === "page" && "text" in item) {
+ return ;
+ } else if (type === "quiz" && "questions" in item) {
+ const quiz = item as { questions: { text: string }[] };
+ return quiz.questions.map((q, i: number) => (
+
+
+
+ ));
+ }
+ return null;
+};
diff --git a/src/app/course/[courseName]/calendar/day/itemInDay/ItemInDay.tsx b/src/app/course/[courseName]/calendar/day/itemInDay/ItemInDay.tsx
new file mode 100644
index 0000000..53ab9bd
--- /dev/null
+++ b/src/app/course/[courseName]/calendar/day/itemInDay/ItemInDay.tsx
@@ -0,0 +1,102 @@
+"use client";
+import { IModuleItem } from "@/features/local/modules/IModuleItem";
+import { getModuleItemUrl } from "@/services/urlUtils";
+import Link from "next/link";
+import { FC, ReactNode, useCallback, useState } from "react";
+import { useCourseContext } from "../../../context/courseContext";
+import { useTooltip } from "@/components/useTooltip";
+import { DraggableItem } from "../../../context/drag/draggingContext";
+import ClientOnly from "@/components/ClientOnly";
+import { useDragStyleContext } from "../../../context/drag/dragStyleContext";
+import { Tooltip } from "../../../../../../components/Tooltip";
+import { DayItemContextMenu } from "./DayItemContextMenu";
+import { GetPreviewContent } from "./GetPreviewContent";
+
+export const ItemInDay: FC<{
+ type: "assignment" | "page" | "quiz";
+ status: "localOnly" | "incomplete" | "published";
+ moduleName: string;
+ item: IModuleItem;
+ message: ReactNode;
+}> = ({ type, moduleName, status, item, message }) => {
+ const { courseName } = useCourseContext();
+ const { setIsDragging } = useDragStyleContext();
+ const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
+
+ const [contextMenuPos, setContextMenuPos] = useState<{
+ x: number;
+ y: number;
+ } | null>(null);
+
+ const handleContextMenu = (e: React.MouseEvent) => {
+ if (type !== "assignment") return;
+ e.preventDefault();
+ e.stopPropagation();
+ setContextMenuPos({ x: e.clientX, y: e.clientY });
+ };
+
+ const closeContextMenu = useCallback(() => {
+ setContextMenuPos(null);
+ }, []);
+
+ return (
+
+ {
+ const draggableItem: DraggableItem = {
+ type,
+ item,
+ sourceModuleName: moduleName,
+ };
+ e.dataTransfer.setData(
+ "draggableItem",
+ JSON.stringify(draggableItem),
+ );
+ setIsDragging(true);
+ }}
+ onMouseEnter={showTooltip}
+ onMouseLeave={hideTooltip}
+ onContextMenu={handleContextMenu}
+ ref={targetRef}
+ >
+ {item.name}
+
+
+ {status === "published" ? (
+
+
+
+ }
+ targetRef={targetRef}
+ visible={visible}
+ />
+ ) : (
+
+ )}
+ {contextMenuPos && type === "assignment" && (
+
+ )}
+
+
+ );
+};
diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx
deleted file mode 100644
index e7c540b..0000000
--- a/src/components/ContextMenu.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-"use client";
-import { useEffect, useRef } from "react";
-
-export interface ContextMenuItem {
- label: string;
- onClick?: () => void;
- href?: string;
- variant?: "default" | "danger";
- disabled?: boolean;
-}
-
-export function ContextMenu({
- x,
- y,
- items,
- onClose,
-}: {
- x: number;
- y: number;
- items: ContextMenuItem[];
- onClose: () => void;
-}) {
- const ref = useRef(null);
-
- useEffect(() => {
- const handleClickOutside = (e: MouseEvent) => {
- if (ref.current && !ref.current.contains(e.target as Node)) {
- onClose();
- }
- };
- const handleEscape = (e: KeyboardEvent) => {
- if (e.key === "Escape") onClose();
- };
- document.addEventListener("mousedown", handleClickOutside);
- document.addEventListener("keydown", handleEscape);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- document.removeEventListener("keydown", handleEscape);
- };
- }, [onClose]);
-
- return (
-
- {items.map((item, i) =>
- item.href ? (
-
- {item.label}
-
- ) : (
-
- )
- )}
-
- );
-}