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: courses:
- path: ./4850_AdvancedFE/2025-fall-alex/modules/
name: Adv Frontend
- path: ./1420/2025-fall-alex/modules/ - path: ./1420/2025-fall-alex/modules/
name: "1420" 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/ - path: ./1425/2025-fall-alex/modules/
name: "1425" name: "1425"
- path: ./4850_AdvancedFE/2026-spring-alex/modules - path: ./4850_AdvancedFE/2026-spring-alex/modules
@@ -25,5 +19,3 @@ courses:
name: distributed-old name: distributed-old
- path: ./3840_Telemetry/2025_spring_alex/modules/ - path: ./3840_Telemetry/2025_spring_alex/modules/
name: telemetry-old 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() { export default function CollapsableSidebar() {
const [windowCollapseRecommended, setWindowCollapseRecommended] = const [windowCollapseRecommended, setWindowCollapseRecommended] =
useState(window.innerWidth <= collapseThreshold); useState(false);
const [userCollapsed, setUserCollapsed] = useState< const [userCollapsed, setUserCollapsed] = useState<
"unset" | "collapsed" | "uncollapsed" "unset" | "collapsed" | "uncollapsed"
>("unset"); >("unset");
useEffect(() => { useEffect(() => {
// Initialize on mount
setWindowCollapseRecommended(window.innerWidth <= collapseThreshold);
function handleResize() { function handleResize() {
if (window.innerWidth <= collapseThreshold) { if (window.innerWidth <= collapseThreshold) {
setWindowCollapseRecommended(true); setWindowCollapseRecommended(true);

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,14 @@
"use client"; "use client";
import LecturePreview from "../LecturePreview"; 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 { useLecturesSuspenseQuery } from "@/features/local/lectures/lectureHooks";
import { BreadCrumbs } from "@/components/BreadCrumbs";
export default function LecturePreviewPage({ export default function LecturePreviewPage({
lectureDay, lectureDay,
}: { }: {
lectureDay: string; lectureDay: string;
}) { }) {
const { courseName } = useCourseContext();
const { data: weeks } = useLecturesSuspenseQuery(); const { data: weeks } = useLecturesSuspenseQuery();
const lecture = weeks const lecture = weeks
.flatMap(({ lectures }) => lectures.map((lecture) => lecture)) .flatMap(({ lectures }) => lectures.map((lecture) => lecture))
@@ -23,20 +20,7 @@ export default function LecturePreviewPage({
return ( return (
<div className="flex h-full xl:flex-row flex-col "> <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="flex-shrink flex-1 pb-1 ms-3 xl:ms-0 flex flex-row flex-wrap gap-3 content-start ">
<div className=""> <BreadCrumbs />
<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>
</div> </div>
<div className="flex justify-center min-h-0 px-2"> <div className="flex justify-center min-h-0 px-2">
<div <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 { UpdateAssignmentName } from "./UpdateAssignmentName";
import { getCourseUrl } from "@/services/urlUtils"; import { RightSingleChevron } from "@/components/icons/RightSingleChevron";
import Link from "next/link";
export default function EditAssignmentHeader({ export default function EditAssignmentHeader({
moduleName, moduleName,
@@ -10,22 +9,21 @@ export default function EditAssignmentHeader({
assignmentName: string; assignmentName: string;
moduleName: string; moduleName: string;
}) { }) {
const { courseName } = useCourseContext();
return ( return (
<div className="py-1 flex flex-row justify-start gap-3"> <div className="py-1 flex flex-row justify-between">
<Link <div className="flex flex-row">
className="btn" <BreadCrumbs />
href={getCourseUrl(courseName)} <span className="text-slate-500 cursor-default select-none my-auto">
shallow={true} <RightSingleChevron />
prefetch={true} </span>
> <div className="my-auto px-3">{assignmentName}</div>
{courseName} </div>
</Link> <div className="px-1">
<UpdateAssignmentName <UpdateAssignmentName
assignmentName={assignmentName} assignmentName={assignmentName}
moduleName={moduleName} moduleName={moduleName}
/> />
<div className="my-auto">{assignmentName}</div> </div>
</div> </div>
); );
} }

View File

@@ -40,8 +40,7 @@ export function UpdateAssignmentName({
if (name === assignmentName) closeModal(); if (name === assignmentName) closeModal();
setIsLoading(true); // page refresh resets flag setIsLoading(true); // page refresh resets flag
try{ try {
await updateAssignment.mutateAsync({ await updateAssignment.mutateAsync({
assignment: assignment, assignment: assignment,
moduleName, moduleName,
@@ -56,11 +55,22 @@ export function UpdateAssignmentName({
getModuleItemUrl(courseName, moduleName, "assignment", name), getModuleItemUrl(courseName, moduleName, "assignment", name),
{} {}
); );
}finally { } finally {
setIsLoading(false); 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 <TextInput
value={name} value={name}
setValue={setName} 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 { UpdatePageName } from "./UpdatePageName";
import { BreadCrumbs } from "@/components/BreadCrumbs";
import { RightSingleChevron } from "@/components/icons/RightSingleChevron";
export default function EditPageHeader({ export default function EditPageHeader({
moduleName, moduleName,
@@ -10,19 +9,18 @@ export default function EditPageHeader({
pageName: string; pageName: string;
moduleName: string; moduleName: string;
}) { }) {
const { courseName } = useCourseContext();
return ( return (
<div className="py-1 flex flex-row justify-start gap-3"> <div className="py-1 flex flex-row justify-between">
<Link <div className="flex flex-row">
className="btn" <BreadCrumbs />
href={getCourseUrl(courseName)} <span className="text-slate-500 cursor-default select-none my-auto">
shallow={true} <RightSingleChevron />
prefetch={true} </span>
> <div className="my-auto px-3">{pageName}</div>
{courseName} </div>
</Link> <div className="px-1">
<UpdatePageName pageName={pageName} moduleName={moduleName} /> <UpdatePageName pageName={pageName} moduleName={moduleName} />
<div className="my-auto">{pageName}</div> </div>
</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"} /> <TextInput value={name} setValue={setName} label={"Rename Page"} />
<button className="w-full my-3">Save New Name</button> <button className="w-full my-3">Save New Name</button>
{isLoading && <Spinner />} {isLoading && <Spinner />}

View File

@@ -1,7 +1,6 @@
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; import { RightSingleChevron } from "@/components/icons/RightSingleChevron";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import { UpdateQuizName } from "./UpdateQuizName"; import { UpdateQuizName } from "./UpdateQuizName";
import { BreadCrumbs } from "@/components/BreadCrumbs";
export default function EditQuizHeader({ export default function EditQuizHeader({
moduleName, moduleName,
@@ -10,19 +9,18 @@ export default function EditQuizHeader({
quizName: string; quizName: string;
moduleName: string; moduleName: string;
}) { }) {
const { courseName } = useCourseContext();
return ( return (
<div className="py-1 flex flex-row justify-start gap-3"> <div className="py-1 flex flex-row justify-between">
<Link <div className="flex flex-row">
className="btn" <BreadCrumbs />
href={getCourseUrl(courseName)} <span className="text-slate-500 cursor-default select-none my-auto">
shallow={true} <RightSingleChevron />
prefetch={true} </span>
> <div className="my-auto px-3">{quizName}</div>
{courseName} </div>
</Link> <div className="px-1">
<UpdateQuizName quizName={quizName} moduleName={moduleName} /> <UpdateQuizName quizName={quizName} moduleName={moduleName} />
<div>{quizName}</div> </div>
</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"} /> <TextInput value={name} setValue={setName} label={"Rename Quiz"} />
<button className="w-full my-3">Save New Name</button> <button className="w-full my-3">Save New Name</button>
{isLoading && <Spinner />} {isLoading && <Spinner />}

View File

@@ -20,7 +20,7 @@ export default async function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<head></head> <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"> <div className="bg-gray-950 h-screen text-slate-300 w-screen sm:p-1">
<MyToaster /> <MyToaster />
<Suspense> <Suspense>

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" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<g id="SVGRepo_bgCarrier" stroke-width="0"></g> <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g <g
id="SVGRepo_tracerCarrier" id="SVGRepo_tracerCarrier"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
></g> ></g>
<g id="SVGRepo_iconCarrier"> <g id="SVGRepo_iconCarrier">
<path <path
className="stroke-slate-300" className="stroke-slate-300"
d="M9 6L15 12L9 18" d="M9 6L15 12L9 18"
stroke-width="2" strokeWidth="2"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
></path> ></path>
</g> </g>
</svg> </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>
);
};