12 Commits

Author SHA1 Message Date
4d934f27f3 Merge branch 'main' of github.com:alexmickelson/canvasManagement 2026-03-11 08:52:16 -06:00
e960e2fa42 scheduling query invalidation 2026-03-11 08:52:14 -06:00
Jonathan Allen
e3079cbc5a Merge pull request #22 from snow-jallen/main 2026-02-25 13:38:55 -07:00
Jonathan Allen
f5a50fdc02 Ignore .folders from module list, auto-width for rubric columns 2026-02-24 10:04:19 -07:00
aa191fe90b better assignment context menu 2026-02-20 09:45:04 -07:00
ca240811f2 refactoring styling 2026-02-20 09:21:23 -07:00
c224a6a9e2 isolating the context menu more 2026-02-20 09:17:19 -07:00
865e86d11f improving assignment context menu 2026-02-20 09:06:52 -07:00
b62dcb9c62 Merge pull request #21 from snow-jallen/main
looks good. I'll be doing some UX updates on the context menu, but I like the idea
2026-02-20 08:36:47 -07:00
Jonathan Allen
77fde8198e Add context menu for assignments on calendar view 2026-02-19 18:17:13 -07:00
Jonathan Allen
8172724a4f Merge pull request #1 from snow-jallen/fix-rubric-column-layout
Fix rubric layout: points left, description right
2026-02-19 17:32:20 -07:00
Jonathan Allen
07155991aa Fix rubric layout: points left, description right
Reorder rubric columns so points appear on the left and the description
is left-aligned on the right. Extra credit gets its own dedicated column.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 17:29:09 -07:00
10 changed files with 376 additions and 98 deletions

1
.gitignore vendored
View File

@@ -44,3 +44,4 @@ next-env.d.ts
storage/ storage/
temp/ temp/
.claude/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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