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">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
|
import { page } from '$app/state';
|
||||||
import { PublicCourseBottomNav } from '@cio/ui/custom/public-course';
|
import { PublicCourseBottomNav } from '@cio/ui/custom/public-course';
|
||||||
import { courseApi } from '$features/course/api';
|
import { courseApi } from '$features/course/api';
|
||||||
import { getContentRoute, getCourseProgress } from '$features/course/utils/content';
|
import { getContentRoute, getCourseProgress } from '$features/course/utils/content';
|
||||||
@@ -28,11 +29,12 @@
|
|||||||
|
|
||||||
let sheetOpen = $state(false);
|
let sheetOpen = $state(false);
|
||||||
|
|
||||||
|
const currentPath = $derived(path || page.url.pathname);
|
||||||
const courseProgress = $derived(getCourseProgress(courseApi.course));
|
const courseProgress = $derived(getCourseProgress(courseApi.course));
|
||||||
const activeIndex = $derived(resolveActiveNavigableContentIndex(courseApi.course, path));
|
const activeIndex = $derived(resolveActiveNavigableContentIndex(courseApi.course, currentPath));
|
||||||
const activeItem = $derived(getActiveNavigableContent(courseApi.course, path));
|
const activeItem = $derived(getActiveNavigableContent(courseApi.course, currentPath));
|
||||||
const previousItem = $derived(getPreviousNavigableContent(courseApi.course, path));
|
const previousItem = $derived(getPreviousNavigableContent(courseApi.course, currentPath));
|
||||||
const nextItem = $derived(getNextIncompleteNavigableContent(courseApi.course, path));
|
const nextItem = $derived(getNextIncompleteNavigableContent(courseApi.course, currentPath));
|
||||||
|
|
||||||
const positionLabel = $derived(
|
const positionLabel = $derived(
|
||||||
activeIndex >= 0 && courseProgress.total > 0 ? `${activeIndex + 1} / ${courseProgress.total}` : ''
|
activeIndex >= 0 && courseProgress.total > 0 ? `${activeIndex + 1} / ${courseProgress.total}` : ''
|
||||||
@@ -87,4 +89,4 @@
|
|||||||
progressPercent={courseProgress.percent}
|
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));
|
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);
|
const pathSegments = currentPath.split('/').filter(Boolean);
|
||||||
return pathSegments.includes(itemId);
|
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));
|
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);
|
const items = getOrderedNavigableContent(course);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -37,7 +44,10 @@ export function resolveActiveNavigableContentIndex(course: Course | null, curren
|
|||||||
return 0;
|
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 items = getOrderedNavigableContent(course);
|
||||||
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
||||||
if (activeIndex <= 0) {
|
if (activeIndex <= 0) {
|
||||||
@@ -47,7 +57,10 @@ export function getPreviousNavigableContent(course: Course | null, currentPath:
|
|||||||
return items[activeIndex - 1] ?? null;
|
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 items = getOrderedNavigableContent(course);
|
||||||
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
||||||
if (activeIndex < 0) {
|
if (activeIndex < 0) {
|
||||||
@@ -64,7 +77,10 @@ export function getNextIncompleteNavigableContent(course: Course | null, current
|
|||||||
return null;
|
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 items = getOrderedNavigableContent(course);
|
||||||
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
const activeIndex = resolveActiveNavigableContentIndex(course, currentPath);
|
||||||
if (activeIndex < 0) {
|
if (activeIndex < 0) {
|
||||||
@@ -93,7 +109,7 @@ export function getSectionIdForContentItem(course: Course | null, itemId: string
|
|||||||
return null;
|
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);
|
const activeItem = getActiveNavigableContent(course, currentPath);
|
||||||
if (!activeItem) {
|
if (!activeItem) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -45,7 +45,6 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: import('svelte').Snippet;
|
children?: import('svelte').Snippet;
|
||||||
path: string;
|
|
||||||
data: {
|
data: {
|
||||||
course?: Course;
|
course?: Course;
|
||||||
courseId: string;
|
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 sidebarWidth = $state(COURSE_SIDEBAR_DEFAULT_WIDTH);
|
||||||
let hasLoadedSidebarWidth = $state(false);
|
let hasLoadedSidebarWidth = $state(false);
|
||||||
let sidebarProviderElement = $state<HTMLDivElement | null>(null);
|
let sidebarProviderElement = $state<HTMLDivElement | null>(null);
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
style={`--sidebar-width: ${sidebarWidth}px; --side-panel-width: ${sidePanel.width}px;`}
|
style={`--sidebar-width: ${sidebarWidth}px; --side-panel-width: ${sidePanel.width}px;`}
|
||||||
>
|
>
|
||||||
<CourseSidebar
|
<CourseSidebar
|
||||||
{path}
|
path={currentPath}
|
||||||
id={data.courseId}
|
id={data.courseId}
|
||||||
{isCourseReady}
|
{isCourseReady}
|
||||||
{sidebarWidth}
|
{sidebarWidth}
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showMobileBottomNav}
|
{#if showMobileBottomNav}
|
||||||
<CourseMobileBottomNav courseId={data.courseId} {path} />
|
<CourseMobileBottomNav courseId={data.courseId} path={currentPath} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Sidebar.Inset>
|
</Sidebar.Inset>
|
||||||
|
|||||||
Reference in New Issue
Block a user