mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 15:18:32 -06:00
adding breadcrumbs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
98
src/components/BreadCrumbs.tsx
Normal file
98
src/components/BreadCrumbs.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
23
src/components/icons/HomeIcon.tsx
Normal file
23
src/components/icons/HomeIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
src/components/icons/RightSingleChevron.tsx
Normal file
27
src/components/icons/RightSingleChevron.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user