diff --git a/src/app/course/[courseName]/calendar/day/itemInDay/DayItemContextMenu.tsx b/src/app/course/[courseName]/calendar/day/itemInDay/DayItemContextMenu.tsx index 4738092..7585295 100644 --- a/src/app/course/[courseName]/calendar/day/itemInDay/DayItemContextMenu.tsx +++ b/src/app/course/[courseName]/calendar/day/itemInDay/DayItemContextMenu.tsx @@ -1,5 +1,5 @@ "use client"; -import { useRef, useEffect, FC, useState } from "react"; +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"; @@ -15,6 +15,7 @@ import { 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"; function getDuplicateName(name: string, existingNames: string[]): string { const match = name.match(/^(.*)\s+(\d+)$/); @@ -27,15 +28,12 @@ function getDuplicateName(name: string, existingNames: string[]): string { return `${baseName} ${num}`; } -export const DayItemContextMenu: FC<{ - x: number; - y: number; - onClose: () => void; +export const AssignmentDayItemContextMenu: FC<{ + modalControl: ModalControl; item: IModuleItem; moduleName: string; -}> = ({ x, y, onClose, item, moduleName }) => { +}> = ({ modalControl, item, moduleName }) => { const { courseName } = useCourseContext(); - const ref = useRef(null); const calendarItems = useCalendarItemsContext(); const createAssignment = useCreateAssignmentMutation(); const deleteLocal = useDeleteAssignmentMutation(); @@ -55,29 +53,21 @@ export const DayItemContextMenu: FC<{ : 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(); + modalControl.closeModal(); } }; - document.addEventListener("mousedown", handleClickOutside); document.addEventListener("keydown", handleEscape); return () => { - document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleEscape); }; - }, [onClose]); + }, [modalControl]); const handleClose = () => { setConfirmingDelete(false); - onClose(); + modalControl.closeModal(); }; const handleDuplicate = () => { @@ -120,85 +110,82 @@ export const DayItemContextMenu: FC<{ } }; - const baseButtonClasses = "unstyled w-full text-left px-4 py-2"; + const baseButtonClasses = "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 && ( + {confirmingDelete ? ( <> - - View in Canvas - + + + ) : ( + <> + {canvasUrl && ( + <> + + View in Canvas + + + + + )} + {!canvasUrl && ( + + )} + )} - {!canvasUrl && ( - - )} - )} -
+ ); }; diff --git a/src/app/course/[courseName]/calendar/day/itemInDay/ItemInDay.tsx b/src/app/course/[courseName]/calendar/day/itemInDay/ItemInDay.tsx index 53ab9bd..1c67ef9 100644 --- a/src/app/course/[courseName]/calendar/day/itemInDay/ItemInDay.tsx +++ b/src/app/course/[courseName]/calendar/day/itemInDay/ItemInDay.tsx @@ -2,15 +2,16 @@ 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 { FC, ReactNode } 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 { AssignmentDayItemContextMenu } from "./DayItemContextMenu"; import { GetPreviewContent } from "./GetPreviewContent"; +import { useModal } from "@/components/Modal"; export const ItemInDay: FC<{ type: "assignment" | "page" | "quiz"; @@ -22,23 +23,15 @@ export const ItemInDay: FC<{ const { courseName } = useCourseContext(); const { setIsDragging } = useDragStyleContext(); const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500); - - const [contextMenuPos, setContextMenuPos] = useState<{ - x: number; - y: number; - } | null>(null); + const modalControl = useModal(); const handleContextMenu = (e: React.MouseEvent) => { if (type !== "assignment") return; e.preventDefault(); e.stopPropagation(); - setContextMenuPos({ x: e.clientX, y: e.clientY }); + modalControl.openModal({ x: e.clientX, y: e.clientY }); }; - const closeContextMenu = useCallback(() => { - setContextMenuPos(null); - }, []); - return (
)} - {contextMenuPos && type === "assignment" && ( - diff --git a/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/EditQuiz.tsx b/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/EditQuiz.tsx index 235ce6b..5f8d7f7 100644 --- a/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/EditQuiz.tsx +++ b/src/app/course/[courseName]/modules/[moduleName]/quiz/[quizName]/EditQuiz.tsx @@ -116,7 +116,7 @@ export default function EditQuiz({ const updateQuizMutation = useUpdateQuizMutation(); const { data: globalSettings } = useGlobalSettingsQuery(); const feedbackDelimiters = getFeedbackDelimitersFromSettings( - (globalSettings ?? ({} as GlobalSettings)) as GlobalSettings + (globalSettings ?? ({} as GlobalSettings)) as GlobalSettings, ); const { clientIsAuthoritative, text, textUpdate, monacoKey } = @@ -141,14 +141,14 @@ export default function EditQuiz({ quizMarkdownUtils.toMarkdown(quiz, feedbackDelimiters) !== quizMarkdownUtils.toMarkdown( quizMarkdownUtils.parseMarkdown(text, name, feedbackDelimiters), - feedbackDelimiters + feedbackDelimiters, ) ) { if (clientIsAuthoritative) { const updatedQuiz = quizMarkdownUtils.parseMarkdown( text, quizName, - feedbackDelimiters + feedbackDelimiters, ); await updateQuizMutation.mutateAsync({ quiz: updatedQuiz, @@ -160,7 +160,7 @@ export default function EditQuiz({ }); } else { console.log( - "client not authoritative, updating client with server quiz" + "client not authoritative, updating client with server quiz", ); textUpdate(quizMarkdownUtils.toMarkdown(quiz), true); } @@ -178,6 +178,7 @@ export default function EditQuiz({ }, [ clientIsAuthoritative, courseName, + feedbackDelimiters, isFetching, moduleName, quiz, diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index a2fbcb6..2e56bce 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -3,23 +3,35 @@ import React, { ReactNode, useCallback, useMemo, useState } from "react"; export interface ModalControl { isOpen: boolean; - openModal: () => void; + openModal: (position?: { x: number; y: number }) => void; closeModal: () => void; + position?: { x: number; y: number }; } export function useModal() { const [isOpen, setIsOpen] = useState(false); + const [position, setPosition] = useState< + { x: number; y: number } | undefined + >(undefined); - const openModal = useCallback(() => setIsOpen(true), []); - const closeModal = useCallback(() => setIsOpen(false), []); + const openModal = useCallback((pos?: { x: number; y: number }) => { + setPosition(pos); + setIsOpen(true); + }, []); + + const closeModal = useCallback(() => { + setIsOpen(false); + setPosition(undefined); + }, []); return useMemo( () => ({ isOpen, openModal, closeModal, + position, }), - [closeModal, isOpen, openModal] + [closeModal, isOpen, openModal, position], ); } @@ -40,18 +52,21 @@ export default function Modal({ }) { return ( <> - {buttonComponent ? ( - buttonComponent({ openModal: modalControl.openModal }) - ) : ( - - )} + {buttonComponent + ? buttonComponent({ openModal: () => modalControl.openModal() }) + : buttonText && ( + + )}
{ e.stopPropagation(); }} - className={ - ` bg-slate-800 p-6 rounded-lg shadow-lg ` + - modalWidth + - ` transition-all duration-400 ` + - ` ${modalControl.isOpen ? "opacity-100" : "opacity-0"}` + 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"}`} + style={ + modalControl.position + ? { + position: "fixed", + left: modalControl.position.x, + top: modalControl.position.y, + } + : undefined } > {modalControl.isOpen &&