fix(course): stop mobile sidebar crash when path is undefined (#934)
The course layout treated `path` as a SvelteKit prop, but parents never pass it. CourseMobileBottomNav then called isContentItemInPath with undefined, which crashed on `.split()` and prevented the mobile sidebar from opening. Use page.url.pathname and guard missing paths.
This commit is contained in:
+7
-5
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { PublicCourseBottomNav } from '@cio/ui/custom/public-course';
|
||||
import { courseApi } from '$features/course/api';
|
||||
import { getContentRoute, getCourseProgress } from '$features/course/utils/content';
|
||||
@@ -28,11 +29,12 @@
|
||||
|
||||
let sheetOpen = $state(false);
|
||||
|
||||
const currentPath = $derived(path || page.url.pathname);
|
||||
const courseProgress = $derived(getCourseProgress(courseApi.course));
|
||||
const activeIndex = $derived(resolveActiveNavigableContentIndex(courseApi.course, path));
|
||||
const activeItem = $derived(getActiveNavigableContent(courseApi.course, path));
|
||||
const previousItem = $derived(getPreviousNavigableContent(courseApi.course, path));
|
||||
const nextItem = $derived(getNextIncompleteNavigableContent(courseApi.course, path));
|
||||
const activeIndex = $derived(resolveActiveNavigableContentIndex(courseApi.course, currentPath));
|
||||
const activeItem = $derived(getActiveNavigableContent(courseApi.course, currentPath));
|
||||
const previousItem = $derived(getPreviousNavigableContent(courseApi.course, currentPath));
|
||||
const nextItem = $derived(getNextIncompleteNavigableContent(courseApi.course, currentPath));
|
||||
|
||||
const positionLabel = $derived(
|
||||
activeIndex >= 0 && courseProgress.total > 0 ? `${activeIndex + 1} / ${courseProgress.total}` : ''
|
||||
@@ -87,4 +89,4 @@
|
||||
progressPercent={courseProgress.percent}
|
||||
/>
|
||||
|
||||
<CourseMobileOutlineSheet bind:open={sheetOpen} {path} {courseId} />
|
||||
<CourseMobileOutlineSheet bind:open={sheetOpen} path={currentPath} {courseId} />
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { isContentItemInPath } from './content-navigation';
|
||||
|
||||
describe('isContentItemInPath', () => {
|
||||
it('returns false when the current path is missing', () => {
|
||||
expect(isContentItemInPath('lesson-1', undefined)).toBe(false);
|
||||
expect(isContentItemInPath('lesson-1', null)).toBe(false);
|
||||
expect(isContentItemInPath('lesson-1', '')).toBe(false);
|
||||
});
|
||||
|
||||
it('matches content ids as exact pathname segments', () => {
|
||||
expect(isContentItemInPath('lesson-1', '/courses/abc/lessons/lesson-1')).toBe(true);
|
||||
expect(isContentItemInPath('lesson-1', '/courses/abc/lessons/lesson-10')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('isContentItemInPath', () => {
|
||||
it('returns false when the current path is missing', () => {
|
||||
expect(isContentItemInPath('lesson-1', undefined)).toBe(false);
|
||||
expect(isContentItemInPath('lesson-1', null)).toBe(false);
|
||||
expect(isContentItemInPath('lesson-1', '')).toBe(false);
|
||||
});
|
||||
|
||||
it('matches content ids as exact pathname segments', () => {
|
||||
expect(isContentItemInPath('lesson-1', '/courses/abc/lessons/lesson-1')).toBe(true);
|
||||
expect(isContentItemInPath('lesson-1', '/courses/abc/lessons/lesson-10')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -9,16 +9,23 @@ export function getFirstIncompleteNavigableContent(course: Course | null): Conte
|
||||
return getOrderedNavigableContent(course).find((item) => !item.isComplete && isNavigableContentUnlocked(item));
|
||||
}
|
||||
|
||||
export function isContentItemInPath(itemId: string, currentPath: string): boolean {
|
||||
export function isContentItemInPath(itemId: string, currentPath: string | null | undefined): boolean {
|
||||
if (!itemId || !currentPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pathSegments = currentPath.split('/').filter(Boolean);
|
||||
return pathSegments.includes(itemId);
|
||||
}
|
||||
|
||||
export function findActiveNavigableContentIndex(items: ContentItem[], currentPath: string): number {
|
||||
export function findActiveNavigableContentIndex(items: ContentItem[], currentPath: string | null | undefined): number {
|
||||
return items.findIndex((item) => isContentItemInPath(item.id, currentPath));
|
||||
}
|
||||
|
||||
export function resolveActiveNavigableContentIndex(course: Course | null, currentPath: string): number {
|
||||
export function resolveActiveNavigableContentIndex(
|
||||
course: Course | null,
|
||||
currentPath: string | null | undefined
|
||||
): number {
|
||||
const items = getOrderedNavigableContent(course);
|
||||
if (items.length === 0) {
|
||||
return -1;
|
||||
@@ -37,7 +44,10 @@ export function resolveActiveNavigableContentIndex(course: Course | null, curren
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getPreviousNavigableContent(course: Course | null, currentPath: string): ContentItem | null {
|
||||
export function getPreviousNavigableContent(
|
||||
course: Course | null,
|
||||
currentPath: string | null | undefined
|
||||
): ContentItem | null {
|
||||
const items = getOrderedNavigableContent(course);
|
||||
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
||||
if (activeIndex <= 0) {
|
||||
@@ -47,7 +57,10 @@ export function getPreviousNavigableContent(course: Course | null, currentPath:
|
||||
return items[activeIndex - 1] ?? null;
|
||||
}
|
||||
|
||||
export function getNextIncompleteNavigableContent(course: Course | null, currentPath: string): ContentItem | null {
|
||||
export function getNextIncompleteNavigableContent(
|
||||
course: Course | null,
|
||||
currentPath: string | null | undefined
|
||||
): ContentItem | null {
|
||||
const items = getOrderedNavigableContent(course);
|
||||
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
||||
if (activeIndex < 0) {
|
||||
@@ -64,7 +77,10 @@ export function getNextIncompleteNavigableContent(course: Course | null, current
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getActiveNavigableContent(course: Course | null, currentPath: string): ContentItem | null {
|
||||
export function getActiveNavigableContent(
|
||||
course: Course | null,
|
||||
currentPath: string | null | undefined
|
||||
): ContentItem | null {
|
||||
const items = getOrderedNavigableContent(course);
|
||||
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
||||
if (activeIndex < 0) {
|
||||
@@ -93,7 +109,7 @@ export function getSectionIdForContentItem(course: Course | null, itemId: string
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getActiveSectionId(course: Course | null, currentPath: string): string | null {
|
||||
export function getActiveSectionId(course: Course | null, currentPath: string | null | undefined): string | null {
|
||||
const activeItem = getActiveNavigableContent(course, currentPath);
|
||||
if (!activeItem) {
|
||||
return null;
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
path: string;
|
||||
data: {
|
||||
course?: Course;
|
||||
courseId: string;
|
||||
@@ -53,7 +52,8 @@
|
||||
};
|
||||
}
|
||||
|
||||
let { children, path, data }: Props = $props();
|
||||
let { children, data }: Props = $props();
|
||||
const currentPath = $derived(page.url.pathname);
|
||||
let sidebarWidth = $state(COURSE_SIDEBAR_DEFAULT_WIDTH);
|
||||
let hasLoadedSidebarWidth = $state(false);
|
||||
let sidebarProviderElement = $state<HTMLDivElement | null>(null);
|
||||
@@ -208,7 +208,7 @@
|
||||
style={`--sidebar-width: ${sidebarWidth}px; --side-panel-width: ${sidePanel.width}px;`}
|
||||
>
|
||||
<CourseSidebar
|
||||
{path}
|
||||
path={currentPath}
|
||||
id={data.courseId}
|
||||
{isCourseReady}
|
||||
{sidebarWidth}
|
||||
@@ -243,7 +243,7 @@
|
||||
{/if}
|
||||
|
||||
{#if showMobileBottomNav}
|
||||
<CourseMobileBottomNav courseId={data.courseId} {path} />
|
||||
<CourseMobileBottomNav courseId={data.courseId} path={currentPath} />
|
||||
{/if}
|
||||
{/if}
|
||||
</Sidebar.Inset>
|
||||
|
||||
Reference in New Issue
Block a user