fix(course): restore mobile mark complete and outline progress card (#942)
* fix(course): pass progress into mobile outline sheet card
The outline sheet bound CourseProgressCard with {courseProgress}, which
sets a courseProgress prop the card does not read. progress was
undefined, so opening the sheet crashed on progress.percent. Pass
progress={courseProgress} and treat missing progress as zeros.
Co-authored-by: Rotimi Best <[email protected]>
* fix(course): keep mark complete on mobile student lessons
The mobile bottom nav hid the whole lesson action row, so students had no way to mark a lesson complete. Hide only prev/next when the bottom bar is shown.
Co-authored-by: Rotimi Best <[email protected]>
* fix(course): show a check on the right of a completed lesson title
When a student marks a lesson complete, hide the disabled Mark as Complete
button and place a filled check icon on the right of the lesson title.
Co-authored-by: Rotimi Best <[email protected]>
* fix(course): show completion check on mobile bottom-nav title
Move the filled check from the lesson page heading to the right of
the active lesson title in the mobile bottom bar.
Co-authored-by: Rotimi Best <[email protected]>
---------
This commit is contained in:
co-authored by
Rotimi Best
parent
4fae088626
commit
44f09bbe6b
@@ -4,27 +4,33 @@
|
|||||||
import type { ContentProgress } from '$features/course/utils/content';
|
import type { ContentProgress } from '$features/course/utils/content';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
progress: ContentProgress;
|
progress?: ContentProgress;
|
||||||
class?: string;
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { progress, class: className = '' }: Props = $props();
|
let { progress, class: className = '' }: Props = $props();
|
||||||
|
|
||||||
|
const percent = $derived(progress?.percent ?? 0);
|
||||||
|
const lessonsComplete = $derived(progress?.lessonsComplete ?? 0);
|
||||||
|
const lessonsTotal = $derived(progress?.lessonsTotal ?? 0);
|
||||||
|
const exercisesComplete = $derived(progress?.exercisesComplete ?? 0);
|
||||||
|
const exercisesTotal = $derived(progress?.exercisesTotal ?? 0);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={className}>
|
<div class={className}>
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="text-xs font-medium">{$t('course.sidebar.progress.title')}</span>
|
<span class="text-xs font-medium">{$t('course.sidebar.progress.title')}</span>
|
||||||
<span class="ui:text-primary text-xs font-semibold tabular-nums">{progress.percent}%</span>
|
<span class="ui:text-primary text-xs font-semibold tabular-nums">{percent}%</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={progress.percent} max={100} class="ui:h-1.5 mt-2" />
|
<Progress value={percent} max={100} class="ui:h-1.5 mt-2" />
|
||||||
<p class="ui:text-muted-foreground mt-2 truncate text-[11px]">
|
<p class="ui:text-muted-foreground mt-2 truncate text-[11px]">
|
||||||
{$t('course.sidebar.progress.lessons', {
|
{$t('course.sidebar.progress.lessons', {
|
||||||
completed: progress.lessonsComplete,
|
completed: lessonsComplete,
|
||||||
total: progress.lessonsTotal
|
total: lessonsTotal
|
||||||
})}{#if progress.exercisesTotal > 0}
|
})}{#if exercisesTotal > 0}
|
||||||
· {$t('course.sidebar.progress.exercises', {
|
· {$t('course.sidebar.progress.exercises', {
|
||||||
completed: progress.exercisesComplete,
|
completed: exercisesComplete,
|
||||||
total: progress.exercisesTotal
|
total: exercisesTotal
|
||||||
})}{/if}
|
})}{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+40
-36
@@ -29,9 +29,11 @@
|
|||||||
courseId: string;
|
courseId: string;
|
||||||
/** When on an exercise page, pass this to show prev/next content in content order (no mark-complete). */
|
/** When on an exercise page, pass this to show prev/next content in content order (no mark-complete). */
|
||||||
exerciseId?: string;
|
exerciseId?: string;
|
||||||
|
/** When false, hide prev/next chevrons (mobile bottom nav already has them). */
|
||||||
|
showPrevNext?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { lessonId, courseId, exerciseId }: Props = $props();
|
let { lessonId, courseId, exerciseId, showPrevNext = true }: Props = $props();
|
||||||
|
|
||||||
let isMarkingComplete = $state(false);
|
let isMarkingComplete = $state(false);
|
||||||
|
|
||||||
@@ -229,7 +231,7 @@
|
|||||||
{watchProgressTooltip}
|
{watchProgressTooltip}
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
{:else if showMarkComplete && lessonId && !isLessonLocked && (showVideoWatchCompleteState || !isVideoWatchLesson)}
|
{:else if showMarkComplete && lessonId && !isLessonLocked && !isLessonComplete && !isVideoWatchLesson}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -242,40 +244,42 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
{#if showPrevNext}
|
||||||
<Tooltip.Root>
|
<div class="flex items-center gap-1">
|
||||||
<Tooltip.Trigger>
|
<Tooltip.Root>
|
||||||
<Button
|
<Tooltip.Trigger>
|
||||||
size="icon-sm"
|
<Button
|
||||||
variant="outline"
|
size="icon-sm"
|
||||||
onclick={() => goToContent(prevNextContent.prev)}
|
variant="outline"
|
||||||
disabled={isPrevDisabled || isPrevNavBlocked}
|
onclick={() => goToContent(prevNextContent.prev)}
|
||||||
aria-label={$t('course.navItem.lessons.prev')}
|
disabled={isPrevDisabled || isPrevNavBlocked}
|
||||||
>
|
aria-label={$t('course.navItem.lessons.prev')}
|
||||||
<ChevronLeftIcon size={14} />
|
>
|
||||||
</Button>
|
<ChevronLeftIcon size={14} />
|
||||||
</Tooltip.Trigger>
|
</Button>
|
||||||
<Tooltip.Content side="bottom" sideOffset={4}>
|
</Tooltip.Trigger>
|
||||||
{prevNavTooltip}
|
<Tooltip.Content side="bottom" sideOffset={4}>
|
||||||
</Tooltip.Content>
|
{prevNavTooltip}
|
||||||
</Tooltip.Root>
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger>
|
<Tooltip.Trigger>
|
||||||
<Button
|
<Button
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => goToContent(prevNextContent.next)}
|
onclick={() => goToContent(prevNextContent.next)}
|
||||||
disabled={isNextDisabled || isNextNavBlocked}
|
disabled={isNextDisabled || isNextNavBlocked}
|
||||||
aria-label={$t('course.navItem.lessons.next')}
|
aria-label={$t('course.navItem.lessons.next')}
|
||||||
>
|
>
|
||||||
<ChevronRightIcon size={14} />
|
<ChevronRightIcon size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content side="bottom" sideOffset={4}>
|
<Tooltip.Content side="bottom" sideOffset={4}>
|
||||||
{nextNavTooltip}
|
{nextNavTooltip}
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</Tooltip.Provider>
|
</Tooltip.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
@@ -77,6 +77,7 @@
|
|||||||
<PublicCourseBottomNav
|
<PublicCourseBottomNav
|
||||||
{positionLabel}
|
{positionLabel}
|
||||||
sublineLabel={activeItem?.title}
|
sublineLabel={activeItem?.title}
|
||||||
|
sublineComplete={activeItem?.isComplete ?? false}
|
||||||
hasPrev={!isPreviousDisabled}
|
hasPrev={!isPreviousDisabled}
|
||||||
hasNext={!isNextDisabled}
|
hasNext={!isNextDisabled}
|
||||||
onPrev={() => goToContent(previousItem)}
|
onPrev={() => goToContent(previousItem)}
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@
|
|||||||
</Drawer.Header>
|
</Drawer.Header>
|
||||||
|
|
||||||
<div class="shrink-0 px-3 pb-2">
|
<div class="shrink-0 px-3 pb-2">
|
||||||
<CourseProgressCard {courseProgress} class="rounded-lg border p-3" />
|
<CourseProgressCard progress={courseProgress} class="rounded-lg border p-3" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div bind:this={scrollContainer} class="flex-1 overflow-y-auto px-2 pb-[max(env(safe-area-inset-bottom),1rem)]">
|
<div bind:this={scrollContainer} class="flex-1 overflow-y-auto px-2 pb-[max(env(safe-area-inset-bottom),1rem)]">
|
||||||
|
|||||||
@@ -433,8 +433,8 @@
|
|||||||
</Page.HeaderContent>
|
</Page.HeaderContent>
|
||||||
<Page.Action>
|
<Page.Action>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if mode === MODES.view && $isCourseLearnerView && !showMobileBottomNav}
|
{#if mode === MODES.view && $isCourseLearnerView}
|
||||||
<ContentNavigationActions {lessonId} {courseId} />
|
<ContentNavigationActions {lessonId} {courseId} showPrevNext={!showMobileBottomNav} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
|
|||||||
@@ -124,6 +124,25 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Bottom nav · completed lesson">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="ui:relative ui:min-h-[200px] ui:bg-background">
|
||||||
|
<PublicCourse.PublicCourseBottomNav
|
||||||
|
positionLabel="2 / 6"
|
||||||
|
sublineLabel="Delving into Data Analysis with Pandas"
|
||||||
|
sublineComplete
|
||||||
|
hasPrev
|
||||||
|
hasNext
|
||||||
|
centerVariant="ring"
|
||||||
|
progressPercent={33}
|
||||||
|
onPrev={() => console.log('prev')}
|
||||||
|
onNext={() => console.log('next')}
|
||||||
|
onOpenSheet={() => console.log('open sheet')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
<Story name="Mobile sheet · collapse + long course">
|
<Story name="Mobile sheet · collapse + long course">
|
||||||
{#snippet template()}
|
{#snippet template()}
|
||||||
{@const activeSlug = 'module-10-lesson-4'}
|
{@const activeSlug = 'module-10-lesson-4'}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
/** Current position — e.g. "3 of 12" — plus a subline — e.g. "Hallucination & limitations". */
|
/** Current position — e.g. "3 of 12" — plus a subline — e.g. "Hallucination & limitations". */
|
||||||
positionLabel: string;
|
positionLabel: string;
|
||||||
sublineLabel?: string;
|
sublineLabel?: string;
|
||||||
|
/** When true, a filled check is shown to the right of `sublineLabel`. */
|
||||||
|
sublineComplete?: boolean;
|
||||||
hasPrev: boolean;
|
hasPrev: boolean;
|
||||||
hasNext: boolean;
|
hasNext: boolean;
|
||||||
onPrev: () => void;
|
onPrev: () => void;
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
let {
|
let {
|
||||||
positionLabel,
|
positionLabel,
|
||||||
sublineLabel,
|
sublineLabel,
|
||||||
|
sublineComplete = false,
|
||||||
hasPrev,
|
hasPrev,
|
||||||
hasNext,
|
hasNext,
|
||||||
onPrev,
|
onPrev,
|
||||||
@@ -98,7 +101,21 @@
|
|||||||
{positionLabel}
|
{positionLabel}
|
||||||
</span>
|
</span>
|
||||||
{#if sublineLabel}
|
{#if sublineLabel}
|
||||||
<span class="ui:max-w-[55vw] ui:truncate ui:text-sm ui:font-medium ui:text-foreground">{sublineLabel}</span>
|
<span class="ui:flex ui:max-w-[55vw] ui:min-w-0 ui:items-center ui:gap-1">
|
||||||
|
<span class="ui:min-w-0 ui:truncate ui:text-sm ui:font-medium ui:text-foreground">{sublineLabel}</span>
|
||||||
|
{#if sublineComplete}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
class="ui:size-3.5 ui:shrink-0 ui:text-primary"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" fill="currentColor"></circle>
|
||||||
|
<path d="m9 12 2 2 4-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user