adding breadcrumbs

This commit is contained in:
2026-01-05 10:22:12 -07:00
parent 076c0b1025
commit 8c01cb2422
17 changed files with 252 additions and 108 deletions

View File

@@ -1,12 +1,6 @@
courses:
- path: ./4850_AdvancedFE/2025-fall-alex/modules/
name: Adv Frontend
- path: ./1420/2025-fall-alex/modules/
name: "1420"
- path: ./1810/2025-fall-alex/modules/
name: Web Intro
- path: ./1430/2025-fall-alex/modules/
name: UX
- path: ./1425/2025-fall-alex/modules/
name: "1425"
- path: ./4850_AdvancedFE/2026-spring-alex/modules
@@ -25,5 +19,3 @@ courses:
name: distributed-old
- path: ./3840_Telemetry/2025_spring_alex/modules/
name: telemetry-old
- path: ./3840_Telemetry/2024Spring_alex/modules/
name: telemetry-old-old

View File

@@ -10,12 +10,15 @@ const collapseThreshold = 1400;
export default function CollapsableSidebar() {
const [windowCollapseRecommended, setWindowCollapseRecommended] =
useState(window.innerWidth <= collapseThreshold);
useState(false);
const [userCollapsed, setUserCollapsed] = useState<
"unset" | "collapsed" | "uncollapsed"
>("unset");
useEffect(() => {
// Initialize on mount
setWindowCollapseRecommended(window.innerWidth <= collapseThreshold);
function handleResize() {
if (window.innerWidth <= collapseThreshold) {
setWindowCollapseRecommended(true);

View File

@@ -1,4 +1,5 @@
"use client";
import { BreadCrumbs } from "@/components/BreadCrumbs";
import { Spinner } from "@/components/Spinner";
import {
useCanvasAssignmentsQuery,
@@ -19,7 +20,6 @@ import {
} from "@/features/canvas/hooks/canvasQuizHooks";
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
export function CourseNavigation() {
const { data: settings } = useLocalCourseSettingsQuery();
@@ -33,9 +33,8 @@ export function CourseNavigation() {
return (
<div className="pb-1 flex flex-row gap-3">
<Link href={"/"} className="btn" shallow={true}>
Back to Course List
</Link>
<BreadCrumbs />
<a
href={`https://snow.instructure.com/courses/${settings.canvasId}`}
className="btn"

View File

@@ -81,19 +81,19 @@ export function DayTitle({ day, dayAsDate }: { day: string; dayAsDate: Date }) {
xmlns="http://www.w3.org/2000/svg"
onClick={openModal}
>
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
<path
d="M6 12H18M12 6V18"
className=" "
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</g>
</svg>

View File

@@ -1,10 +1,11 @@
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
import { getDateFromString } from "@/features/local/utils/timeUtils";
import { getLectureWeekName } from "@/features/local/lectures/lectureUtils";
import { getCourseUrl, getLecturePreviewUrl } from "@/services/urlUtils";
import { getLecturePreviewUrl } from "@/services/urlUtils";
import { useCourseContext } from "../../context/courseContext";
import Link from "next/link";
import { getDayOfWeek } from "@/features/local/course/localCourseSettings";
import { BreadCrumbs } from "@/components/BreadCrumbs";
export default function EditLectureTitle({
lectureDay,
@@ -17,16 +18,7 @@ export default function EditLectureTitle({
const lectureWeekName = getLectureWeekName(settings.startDate, lectureDay);
return (
<div className="flex justify-between sm:flex-row flex-col">
<div className="my-auto">
<Link
className="btn hidden sm:inline"
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
>
{courseName}
</Link>
</div>
<BreadCrumbs />
<div className="flex justify-center ">
<h3 className="mt-auto me-3 text-slate-500 ">Lecture</h3>
<h1 className="">

View File

@@ -1,17 +1,14 @@
"use client";
import LecturePreview from "../LecturePreview";
import { getCourseUrl, getLectureUrl } from "@/services/urlUtils";
import { useCourseContext } from "../../../context/courseContext";
import Link from "next/link";
import { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks";
import { BreadCrumbs } from "@/components/BreadCrumbs";
export default function LecturePreviewPage({
lectureDay,
}: {
lectureDay: string;
}) {
const { courseName } = useCourseContext();
const { data: weeks } = useLecturesSuspenseQuery();
const lecture = weeks
.flatMap(({ lectures }) => lectures.map((lecture) => lecture))
@@ -23,20 +20,7 @@ export default function LecturePreviewPage({
return (
<div className="flex h-full xl:flex-row flex-col ">
<div className="flex-shrink flex-1 pb-1 ms-3 xl:ms-0 flex flex-row flex-wrap gap-3 content-start ">
<div className="">
<Link
className="btn"
href={getLectureUrl(courseName, lectureDay)}
shallow={true}
>
Edit Lecture
</Link>
</div>
<div className="">
<Link className="btn" href={getCourseUrl(courseName)} shallow={true}>
Course Calendar
</Link>
</div>
<BreadCrumbs />
</div>
<div className="flex justify-center min-h-0 px-2">
<div

View File

@@ -1,7 +1,6 @@
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { BreadCrumbs } from "@/components/BreadCrumbs";
import { UpdateAssignmentName } from "./UpdateAssignmentName";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { RightSingleChevron } from "@/components/icons/RightSingleChevron";
export default function EditAssignmentHeader({
moduleName,
@@ -10,22 +9,21 @@ export default function EditAssignmentHeader({
assignmentName: string;
moduleName: string;
}) {
const { courseName } = useCourseContext();
return (
<div className="py-1 flex flex-row justify-start gap-3">
<Link
className="btn"
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
>
{courseName}
</Link>
<UpdateAssignmentName
assignmentName={assignmentName}
moduleName={moduleName}
/>
<div className="my-auto">{assignmentName}</div>
<div className="py-1 flex flex-row justify-between">
<div className="flex flex-row">
<BreadCrumbs />
<span className="text-slate-500 cursor-default select-none my-auto">
<RightSingleChevron />
</span>
<div className="my-auto px-3">{assignmentName}</div>
</div>
<div className="px-1">
<UpdateAssignmentName
assignmentName={assignmentName}
moduleName={moduleName}
/>
</div>
</div>
);
}

View File

@@ -40,8 +40,7 @@ export function UpdateAssignmentName({
if (name === assignmentName) closeModal();
setIsLoading(true); // page refresh resets flag
try{
try {
await updateAssignment.mutateAsync({
assignment: assignment,
moduleName,
@@ -50,17 +49,28 @@ export function UpdateAssignmentName({
previousAssignmentName: assignmentName,
courseName,
});
// update url (will trigger reload...)
router.replace(
getModuleItemUrl(courseName, moduleName, "assignment", name),
{}
);
}finally {
} finally {
setIsLoading(false);
}
}}
>
<div
className="
text-yellow-300
bg-yellow-950/30
border-2
rounded-lg
border-yellow-800
p-1 text-sm mb-2"
>
Warning: does not rename in Canvas
</div>
<TextInput
value={name}
setValue={setName}

View File

@@ -1,7 +1,6 @@
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { UpdatePageName } from "./UpdatePageName";
import { BreadCrumbs } from "@/components/BreadCrumbs";
import { RightSingleChevron } from "@/components/icons/RightSingleChevron";
export default function EditPageHeader({
moduleName,
@@ -10,19 +9,18 @@ export default function EditPageHeader({
pageName: string;
moduleName: string;
}) {
const { courseName } = useCourseContext();
return (
<div className="py-1 flex flex-row justify-start gap-3">
<Link
className="btn"
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
>
{courseName}
</Link>
<UpdatePageName pageName={pageName} moduleName={moduleName} />
<div className="my-auto">{pageName}</div>
<div className="py-1 flex flex-row justify-between">
<div className="flex flex-row">
<BreadCrumbs />
<span className="text-slate-500 cursor-default select-none my-auto">
<RightSingleChevron />
</span>
<div className="my-auto px-3">{pageName}</div>
</div>
<div className="px-1">
<UpdatePageName pageName={pageName} moduleName={moduleName} />
</div>
</div>
);
}

View File

@@ -56,6 +56,17 @@ export function UpdatePageName({
);
}}
>
<div
className="
text-yellow-300
bg-yellow-950/30
border-2
rounded-lg
border-yellow-800
p-1 text-sm mb-2"
>
Warning: does not rename in Canvas
</div>
<TextInput value={name} setValue={setName} label={"Rename Page"} />
<button className="w-full my-3">Save New Name</button>
{isLoading && <Spinner />}

View File

@@ -1,7 +1,6 @@
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { RightSingleChevron } from "@/components/icons/RightSingleChevron";
import { UpdateQuizName } from "./UpdateQuizName";
import { BreadCrumbs } from "@/components/BreadCrumbs";
export default function EditQuizHeader({
moduleName,
@@ -10,19 +9,18 @@ export default function EditQuizHeader({
quizName: string;
moduleName: string;
}) {
const { courseName } = useCourseContext();
return (
<div className="py-1 flex flex-row justify-start gap-3">
<Link
className="btn"
href={getCourseUrl(courseName)}
shallow={true}
prefetch={true}
>
{courseName}
</Link>
<UpdateQuizName quizName={quizName} moduleName={moduleName} />
<div>{quizName}</div>
<div className="py-1 flex flex-row justify-between">
<div className="flex flex-row">
<BreadCrumbs />
<span className="text-slate-500 cursor-default select-none my-auto">
<RightSingleChevron />
</span>
<div className="my-auto px-3">{quizName}</div>
</div>
<div className="px-1">
<UpdateQuizName quizName={quizName} moduleName={moduleName} />
</div>
</div>
);
}

View File

@@ -56,6 +56,17 @@ export function UpdateQuizName({
);
}}
>
<div
className="
text-yellow-300
bg-yellow-950/30
border-2
rounded-lg
border-yellow-800
p-1 text-sm mb-2"
>
Warning: does not rename in Canvas
</div>
<TextInput value={name} setValue={setName} label={"Rename Quiz"} />
<button className="w-full my-3">Save New Name</button>
{isLoading && <Spinner />}

View File

@@ -20,7 +20,7 @@ export default async function RootLayout({
return (
<html lang="en">
<head></head>
<body className="flex justify-center">
<body className="flex justify-center" suppressHydrationWarning>
<div className="bg-gray-950 h-screen text-slate-300 w-screen sm:p-1">
<MyToaster />
<Suspense>
@@ -29,7 +29,7 @@ export default async function RootLayout({
<ClientCacheInvalidation></ClientCacheInvalidation>
{children}
</DataHydration>
</Providers>
</Providers>
</Suspense>
</div>
</body>

View File

@@ -0,0 +1,98 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import HomeIcon from "./icons/HomeIcon";
import { RightSingleChevron } from "./icons/RightSingleChevron";
export const BreadCrumbs = () => {
const pathname = usePathname();
const pathSegments = pathname?.split("/").filter(Boolean) || [];
const isCourseRoute = pathSegments[0] === "course";
const isOnCoursePage = isCourseRoute && pathSegments.length === 2;
const courseName =
isCourseRoute && !isOnCoursePage && pathSegments[1]
? decodeURIComponent(pathSegments[1])
: null;
const isLectureRoute = isCourseRoute && pathSegments[2] === "lecture";
const isOnLecturePage = isLectureRoute && pathSegments.length === 4;
const lectureDate =
isLectureRoute && !isOnLecturePage && pathSegments[3]
? decodeURIComponent(pathSegments[3])
: null;
const sharedBackgroundClassNames = `
group
hover:bg-blue-900/30
rounded-lg
h-full
flex
items-center
transition
`;
const sharedLinkClassNames = `
text-slate-300
transition
group-hover:text-slate-100
rounded-lg
h-full
flex
items-center
px-3
`;
return (
<nav className="flex flex-row font-bold text-sm items-center">
<span className={sharedBackgroundClassNames}>
<Link
href="/"
shallow={true}
className="flex items-center gap-1 rounded-lg h-full "
>
<span className={sharedLinkClassNames}>
<HomeIcon />
</span>
</Link>
</span>
{courseName && (
<>
<span className="text-slate-500 cursor-default select-none">
<RightSingleChevron />
</span>
<span className={sharedBackgroundClassNames}>
<Link
href={`/course/${encodeURIComponent(courseName)}`}
shallow={true}
className={sharedLinkClassNames}
>
{courseName}
</Link>
</span>
</>
)}
{isLectureRoute && lectureDate && courseName && (
<>
<span className="text-slate-500 cursor-default select-none">
<RightSingleChevron />
</span>
<span className={sharedBackgroundClassNames}>
<Link
href={`/course/${encodeURIComponent(
courseName
)}/lecture/${encodeURIComponent(lectureDate)}`}
shallow={true}
className={sharedLinkClassNames}
>
{lectureDate}
</Link>
</span>
</>
)}
</nav>
);
};

View File

@@ -16,19 +16,19 @@ export default function ExpandIcon({
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
<path
className="stroke-slate-300"
d="M9 6L15 12L9 18"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</g>
</svg>

View File

@@ -0,0 +1,23 @@
import React from "react";
// https://www.svgrepo.com/collection/wolf-kit-solid-glyph-icons/?search=home
export default function HomeIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
<path d="M11.3861 1.21065C11.7472 0.929784 12.2528 0.929784 12.6139 1.21065L21.6139 8.21065C21.8575 8.4001 22 8.69141 22 9V20.5C22 21.3284 21.3284 22 20.5 22H15V14C15 13.4477 14.5523 13 14 13H10C9.44772 13 9 13.4477 9 14V22H3.5C2.67157 22 2 21.3284 2 20.5V9C2 8.69141 2.14247 8.4001 2.38606 8.21065L11.3861 1.21065Z"></path>
</g>
</svg>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
// https://www.svgrepo.com/svg/491374/chevron-small-right
export const RightSingleChevron = () => {
return (
<svg
viewBox="7 4 11 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-3 w-3 fill-slate-600"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.08586 5.41412C7.69534 5.80465 7.69534 6.43781 8.08586 6.82834L13.3788 12.1212L8.08586 17.4141C7.69534 17.8046 7.69534 18.4378 8.08586 18.8283L8.79297 19.5354C9.18349 19.926 9.81666 19.926 10.2072 19.5354L16.5607 13.1819C17.1465 12.5961 17.1465 11.6464 16.5607 11.0606L10.2072 4.70702C9.81666 4.31649 9.18349 4.31649 8.79297 4.70702L8.08586 5.41412Z"
></path>
</g>
</svg>
);
};