add previous and next buttons (lectures are separate from assignments/quizzes/pages)

This commit is contained in:
Adam Teichert
2025-12-16 22:27:04 -07:00
parent 859bdf01f2
commit cef2323886
9 changed files with 243 additions and 0 deletions

View 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");
});
});

View 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,
};
}

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

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