mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 23:58:31 -06:00
moving v2 to top level
This commit is contained in:
69
src/app/course/[courseName]/calendar/CalendarMonth.tsx
Normal file
69
src/app/course/[courseName]/calendar/CalendarMonth.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
import { CalendarMonthModel, getWeekNumber } from "./calendarMonthUtils";
|
||||
import { DayOfWeek } from "@/models/local/localCourseSettings";
|
||||
import { Expandable } from "@/components/Expandable";
|
||||
import { CalendarWeek } from "./CalendarWeek";
|
||||
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
|
||||
|
||||
export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => {
|
||||
// const weekInMilliseconds = 604_800_000;
|
||||
const four_days_in_milliseconds = 345_600_000;
|
||||
const [settings] = useLocalCourseSettingsQuery();
|
||||
const startDate = getDateFromStringOrThrow(
|
||||
settings.startDate,
|
||||
"week calculation start date"
|
||||
);
|
||||
|
||||
const pastWeekNumber = getWeekNumber(
|
||||
startDate,
|
||||
new Date(Date.now() - four_days_in_milliseconds)
|
||||
);
|
||||
|
||||
const startOfMonthWeekNumber = getWeekNumber(
|
||||
startDate,
|
||||
new Date(month.year, month.month, 1)
|
||||
);
|
||||
|
||||
const isInPast = pastWeekNumber >= startOfMonthWeekNumber;
|
||||
|
||||
const monthName = new Date(month.year, month.month - 1, 1).toLocaleString(
|
||||
"default",
|
||||
{ month: "long" }
|
||||
);
|
||||
const weekDaysList: DayOfWeek[] = Object.values(DayOfWeek);
|
||||
return (
|
||||
<>
|
||||
<Expandable
|
||||
defaultExpanded={!isInPast}
|
||||
ExpandableElement={({ setIsExpanded }) => (
|
||||
<div className="flex justify-center">
|
||||
<h3
|
||||
className={
|
||||
"text-2xl transition-all duration-500 " +
|
||||
"hover:text-slate-50 underline hover:scale-105 "
|
||||
}
|
||||
onClick={() => setIsExpanded((e) => !e)}
|
||||
role="button"
|
||||
>
|
||||
{monthName}
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="grid grid-cols-7 text-center fw-bold ms-3">
|
||||
{weekDaysList.map((day) => (
|
||||
<div key={day} className={""}>
|
||||
<span className="hidden xl:inline">{day}</span>
|
||||
<span className="xl:hidden inline">{day.slice(0, 3)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{month.daysByWeek.map((week, weekIndex) => (
|
||||
<CalendarWeek key={weekIndex} week={week} monthNumber={month.month} />
|
||||
))}
|
||||
</Expandable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
36
src/app/course/[courseName]/calendar/CalendarWeek.tsx
Normal file
36
src/app/course/[courseName]/calendar/CalendarWeek.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
|
||||
import { getWeekNumber } from "./calendarMonthUtils";
|
||||
import Day from "./day/Day";
|
||||
|
||||
export function CalendarWeek({
|
||||
week,
|
||||
monthNumber,
|
||||
}: {
|
||||
week: string[]; //date strings
|
||||
monthNumber: number;
|
||||
}) {
|
||||
const [settings]= useLocalCourseSettingsQuery();
|
||||
const startDate = getDateFromStringOrThrow(
|
||||
settings.startDate,
|
||||
"week calculation start date"
|
||||
);
|
||||
const firstDateString = getDateFromStringOrThrow(
|
||||
week[0],
|
||||
"week calculation first day of week"
|
||||
);
|
||||
const weekNumber = getWeekNumber(startDate, firstDateString);
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="my-auto text-gray-400 w-6 sm:block hidden">
|
||||
{weekNumber.toString().padStart(2, "0")}
|
||||
</div>
|
||||
<div className="grid grid-cols-7 grow">
|
||||
{week.map((day, dayIndex) => (
|
||||
<Day key={dayIndex} day={day} month={monthNumber} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
src/app/course/[courseName]/calendar/CourseCalendar.tsx
Normal file
46
src/app/course/[courseName]/calendar/CourseCalendar.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
import { getDateFromStringOrThrow } from "@/models/local/utils/timeUtils";
|
||||
import { getMonthsBetweenDates } from "./calendarMonthUtils";
|
||||
import { CalendarMonth } from "./CalendarMonth";
|
||||
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||
import { useMemo } from "react";
|
||||
import CalendarItemsContextProvider from "../context/CalendarItemsContextProvider";
|
||||
|
||||
export default function CourseCalendar() {
|
||||
const [settings] = useLocalCourseSettingsQuery();
|
||||
|
||||
const startDateTime = useMemo(
|
||||
() => getDateFromStringOrThrow(settings.startDate, "course start date"),
|
||||
[settings.startDate]
|
||||
);
|
||||
const endDateTime = useMemo(
|
||||
() => getDateFromStringOrThrow(settings.endDate, "course end date"),
|
||||
[settings.endDate]
|
||||
);
|
||||
const months = useMemo(
|
||||
() => getMonthsBetweenDates(startDateTime, endDateTime),
|
||||
[endDateTime, startDateTime]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
min-h-0
|
||||
flex-grow
|
||||
border-4
|
||||
border-gray-900
|
||||
rounded-lg
|
||||
bg-slate-950
|
||||
sm:p-1
|
||||
"
|
||||
>
|
||||
<div className="h-full overflow-y-scroll sm:pe-1">
|
||||
<CalendarItemsContextProvider>
|
||||
{months.map((month) => (
|
||||
<CalendarMonth key={month.month + "" + month.year} month={month} />
|
||||
))}
|
||||
</CalendarItemsContextProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
108
src/app/course/[courseName]/calendar/calendarMonthUtils.ts
Normal file
108
src/app/course/[courseName]/calendar/calendarMonthUtils.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
dateToMarkdownString,
|
||||
getDateFromStringOrThrow,
|
||||
} from "@/models/local/utils/timeUtils";
|
||||
|
||||
export interface CalendarMonthModel {
|
||||
year: number;
|
||||
month: number;
|
||||
weeks: number[][];
|
||||
daysByWeek: string[][]; //iso date is memo-izable
|
||||
}
|
||||
|
||||
function weeksInMonth(year: number, month: number): number {
|
||||
const firstDayOfMonth = new Date(year, month - 1, 1).getDay();
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
const longDaysInMonth = daysInMonth + firstDayOfMonth;
|
||||
let weeks = Math.floor(longDaysInMonth / 7);
|
||||
if (longDaysInMonth % 7 > 0) {
|
||||
weeks += 1;
|
||||
}
|
||||
return weeks;
|
||||
}
|
||||
|
||||
function createCalendarMonth(year: number, month: number): CalendarMonthModel {
|
||||
const weeksNumber = weeksInMonth(year, month);
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
|
||||
let currentDay = 1;
|
||||
const firstDayOfMonth = new Date(year, month - 1, 1).getDay();
|
||||
|
||||
const daysByWeek = Array.from({ length: weeksNumber })
|
||||
.map((_, weekIndex) =>
|
||||
Array.from({ length: 7 }).map((_, dayIndex) => {
|
||||
if (weekIndex === 0 && dayIndex < firstDayOfMonth) {
|
||||
return dateToMarkdownString(
|
||||
new Date(year, month - 1, dayIndex - firstDayOfMonth + 1, 12, 0, 0)
|
||||
);
|
||||
} else if (currentDay <= daysInMonth) {
|
||||
return dateToMarkdownString(
|
||||
new Date(year, month - 1, currentDay++, 12, 0, 0)
|
||||
);
|
||||
} else {
|
||||
currentDay++;
|
||||
return dateToMarkdownString(
|
||||
new Date(year, month, currentDay - daysInMonth - 1, 12, 0, 0)
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
.filter((week) => {
|
||||
const lastDate = getDateFromStringOrThrow(
|
||||
week.at(-1)!,
|
||||
"filtering out last week of month"
|
||||
);
|
||||
return lastDate.getMonth() <= month - 1;
|
||||
});
|
||||
|
||||
const weeks = daysByWeek.map((week) =>
|
||||
week.map((day) =>
|
||||
getDateFromStringOrThrow(day, "calculating weeks").getDate()
|
||||
)
|
||||
);
|
||||
|
||||
return { year, month, weeks, daysByWeek };
|
||||
}
|
||||
|
||||
export function getMonthsBetweenDates(
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): CalendarMonthModel[] {
|
||||
const monthsInTerm =
|
||||
1 +
|
||||
(endDate.getFullYear() - startDate.getFullYear()) * 12 +
|
||||
endDate.getMonth() -
|
||||
startDate.getMonth();
|
||||
|
||||
return Array.from({ length: monthsInTerm }, (_, monthDiff) => {
|
||||
const month = ((startDate.getMonth() + monthDiff) % 12) + 1;
|
||||
const year =
|
||||
startDate.getFullYear() +
|
||||
Math.floor((startDate.getMonth() + monthDiff) / 12);
|
||||
return createCalendarMonth(year, month);
|
||||
});
|
||||
}
|
||||
|
||||
export const getWeekNumber = (startDate: Date, currentDate: Date) => {
|
||||
const sundayBeforeStartDate = getPreviousSunday(startDate);
|
||||
const daysBetween = daysBetweenDates(sundayBeforeStartDate, currentDate);
|
||||
const weeksDiff = Math.floor(daysBetween / 7);
|
||||
|
||||
if (weeksDiff >= 0) return weeksDiff + 1;
|
||||
return weeksDiff;
|
||||
};
|
||||
|
||||
const daysBetweenDates = (startDate: Date, endDate: Date) => {
|
||||
const diffInTime = endDate.getTime() - startDate.getTime();
|
||||
const diffInDays = diffInTime / (1000 * 3600 * 24);
|
||||
return Math.floor(diffInDays);
|
||||
};
|
||||
|
||||
const getPreviousSunday = (date: Date) => {
|
||||
const result = new Date(date);
|
||||
const dayOfWeek = result.getDay();
|
||||
|
||||
result.setDate(result.getDate() - dayOfWeek);
|
||||
|
||||
return result;
|
||||
};
|
||||
47
src/app/course/[courseName]/calendar/calendarUtils.test.ts
Normal file
47
src/app/course/[courseName]/calendar/calendarUtils.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getWeekNumber } from "./calendarMonthUtils";
|
||||
|
||||
|
||||
// months are 0 based, days are 1 based
|
||||
describe("testing week numbers", () => {
|
||||
|
||||
it("can get before first day", () => {
|
||||
const startDate = new Date(2024, 8, 3);
|
||||
const firstDayOfFirstWeek = new Date(2024, 8, 1);
|
||||
|
||||
const weekNumber = getWeekNumber(startDate, firstDayOfFirstWeek);
|
||||
expect(weekNumber).toBe(1);
|
||||
});
|
||||
|
||||
it("can get end of first week", () => {
|
||||
const startDate = new Date(2024, 8, 3);
|
||||
const firstDayOfFirstWeek = new Date(2024, 8, 7);
|
||||
|
||||
const weekNumber = getWeekNumber(startDate, firstDayOfFirstWeek);
|
||||
expect(weekNumber).toBe(1);
|
||||
});
|
||||
|
||||
it("can get start of second week", () => {
|
||||
const startDate = new Date(2024, 8, 3);
|
||||
const firstDayOfFirstWeek = new Date(2024, 8, 8);
|
||||
|
||||
const weekNumber = getWeekNumber(startDate, firstDayOfFirstWeek);
|
||||
expect(weekNumber).toBe(2);
|
||||
});
|
||||
|
||||
it("can get start of third week", () => {
|
||||
const startDate = new Date(2024, 8, 3);
|
||||
const firstDayOfFirstWeek = new Date(2024, 8, 15);
|
||||
|
||||
const weekNumber = getWeekNumber(startDate, firstDayOfFirstWeek);
|
||||
expect(weekNumber).toBe(3);
|
||||
});
|
||||
it("can get previous week", () => {
|
||||
const startDate = new Date(2024, 8, 3);
|
||||
const firstDayOfFirstWeek = new Date(2024, 7, 29);
|
||||
|
||||
const weekNumber = getWeekNumber(startDate, firstDayOfFirstWeek);
|
||||
expect(weekNumber).toBe(-1);
|
||||
});
|
||||
|
||||
});
|
||||
126
src/app/course/[courseName]/calendar/day/Day.tsx
Normal file
126
src/app/course/[courseName]/calendar/day/Day.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
import {
|
||||
getDateFromStringOrThrow,
|
||||
getDateOnlyMarkdownString,
|
||||
} from "@/models/local/utils/timeUtils";
|
||||
import { useDraggingContext } from "../../context/drag/draggingContext";
|
||||
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||
import { getDayOfWeek } from "@/models/local/localCourseSettings";
|
||||
import { ItemInDay } from "./ItemInDay";
|
||||
import { useTodaysItems } from "./useTodaysItems";
|
||||
import { DayTitle } from "./DayTitle";
|
||||
|
||||
export default function Day({ day, month }: { day: string; month: number }) {
|
||||
const dayAsDate = getDateFromStringOrThrow(
|
||||
day,
|
||||
"calculating same month in day"
|
||||
);
|
||||
const isToday =
|
||||
getDateOnlyMarkdownString(new Date()) ===
|
||||
getDateOnlyMarkdownString(dayAsDate);
|
||||
|
||||
const [settings] = useLocalCourseSettingsQuery();
|
||||
const { itemDropOnDay } = useDraggingContext();
|
||||
|
||||
const { todaysAssignments, todaysQuizzes, todaysPages } = useTodaysItems(day);
|
||||
const isInSameMonth = dayAsDate.getMonth() + 1 == month;
|
||||
const classOnThisDay = settings.daysOfWeek.includes(getDayOfWeek(dayAsDate));
|
||||
|
||||
// maybe this is slow?
|
||||
const holidayNameToday = settings.holidays.reduce(
|
||||
(holidaysHappeningToday, holiday) => {
|
||||
const holidayDates = holiday.days.map((d) =>
|
||||
getDateOnlyMarkdownString(
|
||||
getDateFromStringOrThrow(d, "holiday date in day component")
|
||||
)
|
||||
);
|
||||
const today = getDateOnlyMarkdownString(dayAsDate);
|
||||
|
||||
if (holidayDates.includes(today))
|
||||
return [...holidaysHappeningToday, holiday.name];
|
||||
return holidaysHappeningToday;
|
||||
},
|
||||
[] as string[]
|
||||
);
|
||||
|
||||
const semesterStart = getDateFromStringOrThrow(
|
||||
settings.startDate,
|
||||
"comparing start date in day"
|
||||
);
|
||||
const semesterEnd = getDateFromStringOrThrow(
|
||||
settings.endDate,
|
||||
"comparing end date in day"
|
||||
);
|
||||
|
||||
const isInSemester = semesterStart < dayAsDate && semesterEnd > dayAsDate;
|
||||
|
||||
const meetingClasses =
|
||||
classOnThisDay && isInSemester && holidayNameToday.length === 0
|
||||
? " bg-slate-900 "
|
||||
: " ";
|
||||
|
||||
const todayClasses = isToday
|
||||
? " border border-blue-700 shadow-[0_0px_10px_0px] shadow-blue-500/50 "
|
||||
: " ";
|
||||
|
||||
const monthClass =
|
||||
isInSameMonth && !isToday ? " border border-slate-700 " : " ";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
" rounded-lg sm:m-1 m-0.5 min-h-10 " +
|
||||
meetingClasses +
|
||||
monthClass +
|
||||
todayClasses
|
||||
}
|
||||
onDrop={(e) => itemDropOnDay(e, day)}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="draggingDay flex flex-col">
|
||||
<DayTitle day={day} dayAsDate={dayAsDate} />
|
||||
<div className="flex-grow">
|
||||
{todaysAssignments.map(
|
||||
({ assignment, moduleName, status, message }) => (
|
||||
<ItemInDay
|
||||
key={assignment.name}
|
||||
type={"assignment"}
|
||||
moduleName={moduleName}
|
||||
item={assignment}
|
||||
status={status}
|
||||
message={message}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{todaysQuizzes.map(({ quiz, moduleName, status, message }) => (
|
||||
<ItemInDay
|
||||
key={quiz.name}
|
||||
type={"quiz"}
|
||||
moduleName={moduleName}
|
||||
item={quiz}
|
||||
status={status}
|
||||
message={message}
|
||||
/>
|
||||
))}
|
||||
{todaysPages.map(({ page, moduleName, status, message }) => (
|
||||
<ItemInDay
|
||||
key={page.name}
|
||||
type={"page"}
|
||||
moduleName={moduleName}
|
||||
item={page}
|
||||
status={status}
|
||||
message={message}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
{holidayNameToday.map((n) => (
|
||||
<div key={n} className="font-extrabold text-blue-100 text-center">
|
||||
{n}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
src/app/course/[courseName]/calendar/day/DayTitle.tsx
Normal file
59
src/app/course/[courseName]/calendar/day/DayTitle.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import Modal, { useModal } from "@/components/Modal";
|
||||
import { getLectureUrl } from "@/services/urlUtils";
|
||||
import Link from "next/link";
|
||||
import { useCourseContext } from "../../context/courseContext";
|
||||
import NewItemForm from "../../modules/NewItemForm";
|
||||
import { DraggableItem } from "../../context/drag/draggingContext";
|
||||
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
|
||||
import { getLectureForDay } from "@/models/local/utils/lectureUtils";
|
||||
import { useLecturesSuspenseQuery } from "@/hooks/localCourse/lectureHooks";
|
||||
|
||||
export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) {
|
||||
const { courseName } = useCourseContext();
|
||||
const [weeks] = useLecturesSuspenseQuery();
|
||||
const { setIsDragging } = useDragStyleContext();
|
||||
const todaysLecture = getLectureForDay(weeks, dayAsDate);
|
||||
const modal = useModal();
|
||||
|
||||
const lectureName = todaysLecture && (todaysLecture.name || "lecture");
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<Link
|
||||
className="ms-1 me-1 truncate text-nowrap transition-all hover:font-bold hover:text-slate-300"
|
||||
href={getLectureUrl(courseName, day)}
|
||||
prefetch={false}
|
||||
draggable={true}
|
||||
onDragStart={(e) => {
|
||||
if (todaysLecture) {
|
||||
const draggableItem: DraggableItem = {
|
||||
type: "lecture",
|
||||
item: { ...todaysLecture, dueAt: todaysLecture.date },
|
||||
sourceModuleName: undefined,
|
||||
};
|
||||
e.dataTransfer.setData(
|
||||
"draggableItem",
|
||||
JSON.stringify(draggableItem)
|
||||
);
|
||||
setIsDragging(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dayAsDate.getDate()} {lectureName}
|
||||
</Link>
|
||||
<Modal
|
||||
modalControl={modal}
|
||||
buttonText="+"
|
||||
buttonClass="unstyled hover:font-bold hover:scale-125 px-1 mb-auto mt-0 pt-0"
|
||||
>
|
||||
{({ closeModal }) => (
|
||||
<div>
|
||||
<NewItemForm creationDate={day} onCreate={closeModal} />
|
||||
<br />
|
||||
<button onClick={closeModal}>close</button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
src/app/course/[courseName]/calendar/day/ItemInDay.tsx
Normal file
103
src/app/course/[courseName]/calendar/day/ItemInDay.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { IModuleItem } from "@/models/local/IModuleItem";
|
||||
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||
import Link from "next/link";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { useCourseContext } from "../../context/courseContext";
|
||||
import {
|
||||
useDraggingContext,
|
||||
DraggableItem,
|
||||
} from "../../context/drag/draggingContext";
|
||||
import { createPortal } from "react-dom";
|
||||
import ClientOnly from "@/components/ClientOnly";
|
||||
import { useDragStyleContext } from "../../context/drag/dragStyleContext";
|
||||
|
||||
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 linkRef = useRef<HTMLAnchorElement>(null);
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
return (
|
||||
<div className={" relative group "}>
|
||||
<Link
|
||||
href={getModuleItemUrl(courseName, moduleName, type, item.name)}
|
||||
shallow={true}
|
||||
className={
|
||||
" border rounded-sm sm:px-1 sm:mx-1 break-words mb-1 truncate sm:text-wrap text-nowrap " +
|
||||
" bg-slate-800 " +
|
||||
" block " +
|
||||
(status === "localOnly" && " text-slate-500 border-slate-600 ") +
|
||||
(status === "incomplete" && " border-rose-900 ") +
|
||||
(status === "published" && " border-green-800 ")
|
||||
}
|
||||
role="button"
|
||||
draggable="true"
|
||||
onDragStart={(e) => {
|
||||
const draggableItem: DraggableItem = {
|
||||
type,
|
||||
item,
|
||||
sourceModuleName: moduleName,
|
||||
};
|
||||
e.dataTransfer.setData(
|
||||
"draggableItem",
|
||||
JSON.stringify(draggableItem)
|
||||
);
|
||||
setIsDragging(true)
|
||||
|
||||
}}
|
||||
onMouseEnter={() => setTooltipVisible(true)}
|
||||
onMouseLeave={() => setTooltipVisible(false)}
|
||||
ref={linkRef}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
<ClientOnly>
|
||||
<Tooltip
|
||||
message={message}
|
||||
targetRef={linkRef}
|
||||
visible={tooltipVisible && status === "incomplete"}
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Tooltip: React.FC<{
|
||||
message: ReactNode;
|
||||
targetRef: React.RefObject<HTMLElement>;
|
||||
visible: boolean;
|
||||
}> = ({ message, targetRef, visible }) => {
|
||||
const rect = targetRef.current?.getBoundingClientRect();
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
style={{
|
||||
top: (rect?.bottom ?? 0) + window.scrollY + 10,
|
||||
left: (rect?.left ?? 0) + window.scrollX + (rect?.width ?? 0) / 2,
|
||||
}}
|
||||
className={
|
||||
" absolute -translate-x-1/2 " +
|
||||
" bg-gray-800 text-white text-sm " +
|
||||
" rounded py-1 px-2 " +
|
||||
" transition-all duration-400 " +
|
||||
" border border-slate-700 shadow-[0_0px_10px_0px] shadow-slate-500/50 " +
|
||||
(visible ? " " : " hidden -z-50 ")
|
||||
}
|
||||
role="tooltip"
|
||||
>
|
||||
{message}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
119
src/app/course/[courseName]/calendar/day/getStatus.tsx
Normal file
119
src/app/course/[courseName]/calendar/day/getStatus.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment";
|
||||
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
||||
import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel";
|
||||
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
|
||||
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
|
||||
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
|
||||
import {
|
||||
dateToMarkdownString,
|
||||
getDateFromStringOrThrow,
|
||||
} from "@/models/local/utils/timeUtils";
|
||||
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
||||
import { htmlIsCloseEnough } from "@/services/utils/htmlIsCloseEnough";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const getStatus = ({
|
||||
item,
|
||||
canvasItem,
|
||||
type,
|
||||
settings,
|
||||
}: {
|
||||
item: LocalQuiz | LocalAssignment | LocalCoursePage;
|
||||
canvasItem?: CanvasQuiz | CanvasAssignment | CanvasPage;
|
||||
type: "assignment" | "page" | "quiz";
|
||||
settings: LocalCourseSettings;
|
||||
}): {
|
||||
status: "localOnly" | "incomplete" | "published";
|
||||
message: ReactNode;
|
||||
} => {
|
||||
if (!canvasItem) return { status: "localOnly", message: "not in canvas" };
|
||||
|
||||
if (!canvasItem.published)
|
||||
return { status: "incomplete", message: "not published in canvas" };
|
||||
|
||||
if (type === "page") {
|
||||
const canvasPage = canvasItem as CanvasPage;
|
||||
const page = item as LocalCoursePage;
|
||||
|
||||
if (!canvasPage.published)
|
||||
return { status: "incomplete", message: "canvas page not published" };
|
||||
return { status: "published", message: "" };
|
||||
} else if (type === "quiz") {
|
||||
const quiz = item as LocalQuiz;
|
||||
const canvasQuiz = canvasItem as CanvasQuiz;
|
||||
|
||||
if (!canvasQuiz.due_at)
|
||||
return { status: "incomplete", message: "due date not in canvas" };
|
||||
|
||||
if (quiz.lockAt && !canvasQuiz.lock_at)
|
||||
return { status: "incomplete", message: "lock date not in canvas" };
|
||||
|
||||
const localDueDate = dateToMarkdownString(
|
||||
getDateFromStringOrThrow(quiz.dueAt, "comparing due dates for day")
|
||||
);
|
||||
|
||||
const canvasDueDate = dateToMarkdownString(
|
||||
getDateFromStringOrThrow(
|
||||
canvasQuiz.due_at,
|
||||
"comparing canvas due date for day"
|
||||
)
|
||||
);
|
||||
if (localDueDate !== canvasDueDate) {
|
||||
return {
|
||||
status: "incomplete",
|
||||
message: (
|
||||
<div>
|
||||
due date different
|
||||
<div>{localDueDate}</div>
|
||||
<div>{canvasDueDate}</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
} else if (type === "assignment") {
|
||||
const assignment = item as LocalAssignment;
|
||||
const canvasAssignment = canvasItem as CanvasAssignment;
|
||||
|
||||
if (!canvasAssignment.due_at)
|
||||
return { status: "incomplete", message: "due date not in canvas" };
|
||||
|
||||
if (assignment.lockAt && !canvasAssignment.lock_at)
|
||||
return { status: "incomplete", message: "lock date not in canvas" };
|
||||
|
||||
const localDueDate = dateToMarkdownString(
|
||||
getDateFromStringOrThrow(assignment.dueAt, "comparing due dates for day")
|
||||
);
|
||||
const canvasDueDate = dateToMarkdownString(
|
||||
getDateFromStringOrThrow(
|
||||
canvasAssignment.due_at,
|
||||
"comparing canvas due date for day"
|
||||
)
|
||||
);
|
||||
|
||||
if (localDueDate !== canvasDueDate)
|
||||
return {
|
||||
status: "incomplete",
|
||||
message: (
|
||||
<div>
|
||||
due date different
|
||||
<div>{localDueDate}</div>
|
||||
<div>{canvasDueDate}</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
const htmlIsSame = htmlIsCloseEnough(
|
||||
markdownToHTMLSafe(assignment.description, settings),
|
||||
canvasAssignment.description
|
||||
);
|
||||
if (!htmlIsSame)
|
||||
return {
|
||||
status: "incomplete",
|
||||
message: "Canvas description is different",
|
||||
};
|
||||
}
|
||||
|
||||
return { status: "published", message: "" };
|
||||
};
|
||||
101
src/app/course/[courseName]/calendar/day/useTodaysItems.tsx
Normal file
101
src/app/course/[courseName]/calendar/day/useTodaysItems.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
import { useCanvasAssignmentsQuery } from "@/hooks/canvas/canvasAssignmentHooks";
|
||||
import { useCanvasPagesQuery } from "@/hooks/canvas/canvasPageHooks";
|
||||
import { useCanvasQuizzesQuery } from "@/hooks/canvas/canvasQuizHooks";
|
||||
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
|
||||
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
|
||||
import {
|
||||
getDateFromStringOrThrow,
|
||||
getDateOnlyMarkdownString,
|
||||
} from "@/models/local/utils/timeUtils";
|
||||
import { ReactNode } from "react";
|
||||
import { useCalendarItemsContext } from "../../context/calendarItemsContext";
|
||||
import { getStatus } from "./getStatus";
|
||||
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
|
||||
|
||||
export function useTodaysItems(day: string) {
|
||||
const [settings] = useLocalCourseSettingsQuery();
|
||||
const dayAsDate = getDateFromStringOrThrow(
|
||||
day,
|
||||
"calculating same month in day items"
|
||||
);
|
||||
const itemsContext = useCalendarItemsContext();
|
||||
const dateKey = getDateOnlyMarkdownString(dayAsDate);
|
||||
const todaysModules = itemsContext[dateKey];
|
||||
|
||||
const { data: canvasAssignments } = useCanvasAssignmentsQuery();
|
||||
const { data: canvasQuizzes } = useCanvasQuizzesQuery();
|
||||
const { data: canvasPages } = useCanvasPagesQuery();
|
||||
const todaysAssignments: {
|
||||
moduleName: string;
|
||||
assignment: LocalAssignment;
|
||||
status: "localOnly" | "incomplete" | "published";
|
||||
message: ReactNode;
|
||||
}[] = todaysModules
|
||||
? Object.keys(todaysModules).flatMap((moduleName) =>
|
||||
todaysModules[moduleName].assignments.map((assignment) => {
|
||||
const canvasAssignment = canvasAssignments?.find(
|
||||
(c) => c.name === assignment.name
|
||||
);
|
||||
return {
|
||||
moduleName,
|
||||
assignment,
|
||||
...getStatus({
|
||||
item: assignment,
|
||||
canvasItem: canvasAssignment,
|
||||
type: "assignment",
|
||||
settings,
|
||||
}),
|
||||
};
|
||||
})
|
||||
)
|
||||
: [];
|
||||
|
||||
const todaysQuizzes: {
|
||||
moduleName: string;
|
||||
quiz: LocalQuiz;
|
||||
status: "localOnly" | "incomplete" | "published";
|
||||
message: ReactNode;
|
||||
}[] = todaysModules
|
||||
? Object.keys(todaysModules).flatMap((moduleName) =>
|
||||
todaysModules[moduleName].quizzes.map((quiz) => {
|
||||
const canvasQuiz = canvasQuizzes?.find((q) => q.title === quiz.name);
|
||||
return {
|
||||
moduleName,
|
||||
quiz,
|
||||
...getStatus({
|
||||
item: quiz,
|
||||
canvasItem: canvasQuiz,
|
||||
type: "quiz",
|
||||
settings,
|
||||
}),
|
||||
};
|
||||
})
|
||||
)
|
||||
: [];
|
||||
|
||||
const todaysPages: {
|
||||
moduleName: string;
|
||||
page: LocalCoursePage;
|
||||
status: "localOnly" | "incomplete" | "published";
|
||||
message: ReactNode;
|
||||
}[] = todaysModules
|
||||
? Object.keys(todaysModules).flatMap((moduleName) =>
|
||||
todaysModules[moduleName].pages.map((page) => {
|
||||
const canvasPage = canvasPages?.find((p) => p.title === page.name);
|
||||
return {
|
||||
moduleName,
|
||||
page,
|
||||
...getStatus({
|
||||
item: page,
|
||||
canvasItem: canvasPage,
|
||||
type: "page",
|
||||
settings,
|
||||
}),
|
||||
};
|
||||
})
|
||||
)
|
||||
: [];
|
||||
return { todaysAssignments, todaysQuizzes, todaysPages };
|
||||
}
|
||||
Reference in New Issue
Block a user