mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
add previous and next buttons (lectures are separate from assignments/quizzes/pages)
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ItemNavigationButtons({
|
||||
previousUrl,
|
||||
nextUrl,
|
||||
}: {
|
||||
previousUrl: string | null;
|
||||
nextUrl: string | null;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{previousUrl && (
|
||||
<Link className="btn" href={previousUrl} shallow={true}>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
{nextUrl && (
|
||||
<Link className="btn" href={nextUrl} shallow={true}>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
70
src/app/course/[courseName]/hooks/navigationLogic.test.ts
Normal file
70
src/app/course/[courseName]/hooks/navigationLogic.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
getOrderedItems,
|
||||
getOrderedLectures,
|
||||
getNavigationLinks,
|
||||
OrderedCourseItem,
|
||||
} from "./navigationLogic";
|
||||
import { CalendarItemsInterface } from "../context/calendarItemsContext";
|
||||
|
||||
describe("navigationLogic", () => {
|
||||
const courseName = "testCourse";
|
||||
|
||||
it("getOrderedItems should order items by date, then alphabetically by name", () => {
|
||||
const createMock = (
|
||||
date: string,
|
||||
name: string,
|
||||
key: "assignments" | "quizzes" | "pages"
|
||||
) =>
|
||||
({
|
||||
[date]: { "Module 1": { [key]: [{ name }] } },
|
||||
} as unknown as CalendarItemsInterface);
|
||||
|
||||
const orderedItems = getOrderedItems(
|
||||
courseName,
|
||||
createMock("2023-01-01", "Z Assignment", "assignments"),
|
||||
createMock("2023-01-01", "A Quiz", "quizzes"),
|
||||
createMock("2023-01-02", "B Assignment", "assignments"),
|
||||
createMock("2023-01-02", "A Page", "pages")
|
||||
);
|
||||
|
||||
expect(orderedItems.map((i) => `${i.date} ${i.name}`)).toEqual([
|
||||
"2023-01-01 A Quiz",
|
||||
"2023-01-01 Z Assignment",
|
||||
"2023-01-02 A Page",
|
||||
"2023-01-02 B Assignment",
|
||||
]);
|
||||
});
|
||||
|
||||
it("getNavigationLinks should handle wrapping and normal navigation", () => {
|
||||
const items: OrderedCourseItem[] = [
|
||||
{ type: "assignment", name: "1", moduleName: "M", date: "D", url: "u1" },
|
||||
{ type: "quiz", name: "2", moduleName: "M", date: "D", url: "u2" },
|
||||
{ type: "page", name: "3", moduleName: "M", date: "D", url: "u3" },
|
||||
];
|
||||
|
||||
// Forward wrap (last -> first)
|
||||
expect(getNavigationLinks(items, "page", "3", "M").nextUrl).toBe("u1");
|
||||
|
||||
// Backward wrap (first -> last)
|
||||
expect(getNavigationLinks(items, "assignment", "1", "M").previousUrl).toBe(
|
||||
"u3"
|
||||
);
|
||||
|
||||
// Normal navigation (middle)
|
||||
const middle = getNavigationLinks(items, "quiz", "2", "M");
|
||||
expect(middle.previousUrl).toBe("u1");
|
||||
expect(middle.nextUrl).toBe("u3");
|
||||
});
|
||||
|
||||
it("getOrderedLectures should flatten weeks and generate correct URLs", () => {
|
||||
const weeks = [
|
||||
{ lectures: [{ date: "01/01/2023" }] },
|
||||
{ lectures: [{ date: "01/02/2023" }, { date: "01/03/2023" }] },
|
||||
];
|
||||
const lectures = getOrderedLectures(weeks as any, courseName);
|
||||
expect(lectures).toHaveLength(3);
|
||||
expect(lectures[0].url).toContain(encodeURIComponent("01/01/2023"));
|
||||
expect(lectures[0].type).toBe("lecture");
|
||||
});
|
||||
});
|
||||
83
src/app/course/[courseName]/hooks/navigationLogic.ts
Normal file
83
src/app/course/[courseName]/hooks/navigationLogic.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { CalendarItemsInterface } from "../context/calendarItemsContext";
|
||||
import { getLectureUrl, getModuleItemUrl } from "@/services/urlUtils";
|
||||
|
||||
export type CourseItemType = "assignment" | "quiz" | "page" | "lecture";
|
||||
|
||||
export interface OrderedCourseItem {
|
||||
type: CourseItemType;
|
||||
name: string;
|
||||
moduleName?: string;
|
||||
date: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export function getOrderedItems(
|
||||
courseName: string,
|
||||
...calendars: CalendarItemsInterface[]
|
||||
): OrderedCourseItem[] {
|
||||
const itemTypes = [
|
||||
{ key: "assignments" as const, type: "assignment" as const },
|
||||
{ key: "quizzes" as const, type: "quiz" as const },
|
||||
{ key: "pages" as const, type: "page" as const },
|
||||
];
|
||||
|
||||
return calendars
|
||||
.flatMap((calendar) =>
|
||||
Object.entries(calendar).flatMap(([date, modules]) =>
|
||||
Object.entries(modules).flatMap(([moduleName, moduleData]) =>
|
||||
itemTypes.flatMap(({ key, type }) =>
|
||||
(moduleData[key] || []).map((item) => ({
|
||||
type,
|
||||
name: item.name,
|
||||
moduleName,
|
||||
date,
|
||||
url: getModuleItemUrl(courseName, moduleName, type, item.name),
|
||||
}))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const dateCompare = a.date.localeCompare(b.date);
|
||||
if (dateCompare !== 0) return dateCompare;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
export function getOrderedLectures(
|
||||
weeks: { lectures: { date: string }[] }[],
|
||||
courseName: string
|
||||
): OrderedCourseItem[] {
|
||||
return weeks
|
||||
.flatMap((week) => week.lectures)
|
||||
.map((lecture) => ({
|
||||
type: "lecture",
|
||||
name: lecture.date,
|
||||
date: lecture.date,
|
||||
url: getLectureUrl(courseName, lecture.date),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getNavigationLinks(
|
||||
list: OrderedCourseItem[],
|
||||
type: CourseItemType,
|
||||
name: string,
|
||||
moduleName?: string
|
||||
) {
|
||||
const index = list.findIndex((item) => {
|
||||
if (type === "lecture") return item.date === name;
|
||||
return (
|
||||
item.name === name && item.type === type && item.moduleName === moduleName
|
||||
);
|
||||
});
|
||||
|
||||
if (index === -1) return { previousUrl: null, nextUrl: null };
|
||||
|
||||
const previousIndex = (index - 1 + list.length) % list.length;
|
||||
const nextIndex = (index + 1) % list.length;
|
||||
|
||||
return {
|
||||
previousUrl: list[previousIndex].url,
|
||||
nextUrl: list[nextIndex].url,
|
||||
};
|
||||
}
|
||||
14
src/app/course/[courseName]/hooks/useItemNavigation.ts
Normal file
14
src/app/course/[courseName]/hooks/useItemNavigation.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useOrderedCourseItems } from "./useOrderedCourseItems";
|
||||
import { getNavigationLinks, CourseItemType } from "./navigationLogic";
|
||||
|
||||
export function useItemNavigation(
|
||||
type: CourseItemType,
|
||||
name: string,
|
||||
moduleName?: string
|
||||
) {
|
||||
const { orderedItems, orderedLectures } = useOrderedCourseItems();
|
||||
|
||||
const list = type === "lecture" ? orderedLectures : orderedItems;
|
||||
|
||||
return getNavigationLinks(list, type, name, moduleName);
|
||||
}
|
||||
24
src/app/course/[courseName]/hooks/useOrderedCourseItems.ts
Normal file
24
src/app/course/[courseName]/hooks/useOrderedCourseItems.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
useCourseAssignmentsByModuleByDateQuery,
|
||||
useCoursePagesByModuleByDateQuery,
|
||||
useCourseQuizzesByModuleByDateQuery,
|
||||
} from "@/features/local/modules/localCourseModuleHooks";
|
||||
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks";
|
||||
import { useCourseContext } from "../context/courseContext";
|
||||
import { getOrderedItems, getOrderedLectures } from "./navigationLogic";
|
||||
|
||||
export function useOrderedCourseItems() {
|
||||
const { courseName } = useCourseContext();
|
||||
const { data: weeks } = useLecturesSuspenseQuery();
|
||||
|
||||
const orderedItems = getOrderedItems(
|
||||
courseName,
|
||||
useCourseAssignmentsByModuleByDateQuery(),
|
||||
useCourseQuizzesByModuleByDateQuery(),
|
||||
useCoursePagesByModuleByDateQuery()
|
||||
);
|
||||
|
||||
const orderedLectures = getOrderedLectures(weeks, courseName);
|
||||
|
||||
return { orderedItems, orderedLectures };
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { useCourseContext } from "../../context/courseContext";
|
||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
||||
import { useDeleteLectureMutation } from "@/features/local/lectures/lectureHooks";
|
||||
import Link from "next/link";
|
||||
import { useItemNavigation } from "../../hooks/useItemNavigation";
|
||||
import ItemNavigationButtons from "../../components/ItemNavigationButtons";
|
||||
|
||||
export default function LectureButtons({ lectureDay }: { lectureDay: string }) {
|
||||
const { courseName } = useCourseContext();
|
||||
@@ -17,6 +19,7 @@ export default function LectureButtons({ lectureDay }: { lectureDay: string }) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const modal = useModal();
|
||||
const deleteLecture = useDeleteLectureMutation();
|
||||
const { previousUrl, nextUrl } = useItemNavigation("lecture", lectureDay);
|
||||
|
||||
return (
|
||||
<div className="p-5 flex flex-row justify-end gap-3">
|
||||
@@ -61,6 +64,7 @@ export default function LectureButtons({ lectureDay }: { lectureDay: string }) {
|
||||
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
|
||||
Go Back
|
||||
</Link>
|
||||
<ItemNavigationButtons previousUrl={previousUrl} nextUrl={nextUrl} />
|
||||
{isLoading && <Spinner />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ import { getCourseUrl } from "@/services/urlUtils";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useItemNavigation } from "../../../../hooks/useItemNavigation";
|
||||
import ItemNavigationButtons from "../../../../components/ItemNavigationButtons";
|
||||
|
||||
export function AssignmentFooterButtons({
|
||||
moduleName,
|
||||
@@ -42,6 +44,11 @@ export function AssignmentFooterButtons({
|
||||
const deleteLocal = useDeleteAssignmentMutation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const modal = useModal();
|
||||
const { previousUrl, nextUrl } = useItemNavigation(
|
||||
"assignment",
|
||||
assignmentName,
|
||||
moduleName
|
||||
);
|
||||
|
||||
const assignmentInCanvas = canvasAssignments?.find(
|
||||
(a) => a.name === assignmentName
|
||||
@@ -155,6 +162,7 @@ export function AssignmentFooterButtons({
|
||||
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
|
||||
Go Back
|
||||
</Link>
|
||||
<ItemNavigationButtons previousUrl={previousUrl} nextUrl={nextUrl} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ import { getCourseUrl } from "@/services/urlUtils";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useItemNavigation } from "../../../../hooks/useItemNavigation";
|
||||
import ItemNavigationButtons from "../../../../components/ItemNavigationButtons";
|
||||
|
||||
export default function EditPageButtons({
|
||||
moduleName,
|
||||
@@ -36,6 +38,11 @@ export default function EditPageButtons({
|
||||
const deletePageLocal = useDeletePageMutation();
|
||||
const modal = useModal();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { previousUrl, nextUrl } = useItemNavigation(
|
||||
"page",
|
||||
pageName,
|
||||
moduleName
|
||||
);
|
||||
|
||||
const pageInCanvas = canvasPages?.find((p) => p.title === pageName);
|
||||
|
||||
@@ -125,6 +132,7 @@ export default function EditPageButtons({
|
||||
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
|
||||
Go Back
|
||||
</Link>
|
||||
<ItemNavigationButtons previousUrl={previousUrl} nextUrl={nextUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
import { getCourseUrl } from "@/services/urlUtils";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useItemNavigation } from "../../../../hooks/useItemNavigation";
|
||||
import ItemNavigationButtons from "../../../../components/ItemNavigationButtons";
|
||||
|
||||
export function QuizButtons({
|
||||
moduleName,
|
||||
@@ -35,6 +37,11 @@ export function QuizButtons({
|
||||
const deleteFromCanvas = useDeleteQuizFromCanvasMutation();
|
||||
const deleteLocal = useDeleteQuizMutation();
|
||||
const modal = useModal();
|
||||
const { previousUrl, nextUrl } = useItemNavigation(
|
||||
"quiz",
|
||||
quizName,
|
||||
moduleName
|
||||
);
|
||||
|
||||
const quizInCanvas = canvasQuizzes?.find((c) => c.title === quizName);
|
||||
|
||||
@@ -111,6 +118,7 @@ export function QuizButtons({
|
||||
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
|
||||
Go Back
|
||||
</Link>
|
||||
<ItemNavigationButtons previousUrl={previousUrl} nextUrl={nextUrl} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user