feat: add sequential progression and video watch lesson completion
Gate student content by completion order, track per-video watch progress with seek enforcement, and auto-complete lessons at the configured threshold. Show ring progress in the lesson header, keep watch progress on rewatch, and sync progression locks client-side so the next item unlocks after completion.
This commit is contained in:
@@ -7,6 +7,7 @@ export const load = async ({ params, cookies }) => {
|
||||
const exerciseId = params.exerciseId || '';
|
||||
|
||||
if (!courseId || !exerciseId) {
|
||||
console.log('courseId', courseId, 'exerciseId', exerciseId);
|
||||
return { courseId, exerciseId, exercise: null, submissions: [], mySubmissions: [] };
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ export const load = async ({ params, cookies }) => {
|
||||
const mySubmissions = mySubmissionData;
|
||||
|
||||
const submissions = Array.isArray(overview.allSubmissions) ? overview.allSubmissions : [];
|
||||
console.log('submissions', submissions.length, 'mySubmissions', mySubmissions.length);
|
||||
|
||||
return {
|
||||
courseId,
|
||||
|
||||
@@ -2,19 +2,38 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import LockIcon from '@lucide/svelte/icons/lock';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { ExercisePage } from '$features/course/pages';
|
||||
import { questionnaireMetaData, reset } from '$features/course/components/exercise/store';
|
||||
import { courseApi } from '$features/course/api';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { hydrateExercisePageData } from '$features/course/utils/exercise-page-utils';
|
||||
import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar';
|
||||
import { ContentType } from '@cio/utils/constants/content';
|
||||
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
const path = `/courses/${data.courseId}/lessons`;
|
||||
const isLockedForStudent = $derived($isOrgStudent && data.exercise?.isUnlocked === false);
|
||||
const isCourseReady = $derived(courseApi.course?.id === data.courseId);
|
||||
|
||||
const contentLockReason = $derived(
|
||||
getStudentContentLockReason(courseApi.course, data.exerciseId, ContentType.Exercise)
|
||||
);
|
||||
|
||||
const isLockedForStudent = $derived.by(() => {
|
||||
if (!$isOrgStudent) return false;
|
||||
|
||||
if (data.exercise?.isUnlocked === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!data.exercise || contentLockReason !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!data.exercise || isLockedForStudent) return;
|
||||
@@ -32,14 +51,10 @@
|
||||
</script>
|
||||
|
||||
<Page.Root class="mx-auto flex w-full px-3 sm:w-[90%] sm:px-4 lg:max-w-5xl">
|
||||
{#if isLockedForStudent}
|
||||
<Empty
|
||||
title={$t('course.navItem.lessons.content_locked_title')}
|
||||
description={$t('course.navItem.lessons.content_locked_description')}
|
||||
icon={LockIcon}
|
||||
variant="page"
|
||||
class="text-center"
|
||||
/>
|
||||
{#if $isOrgStudent && !isCourseReady}
|
||||
<div class="flex justify-center py-16">
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
<ExercisePage
|
||||
exerciseId={data.exerciseId}
|
||||
|
||||
Reference in New Issue
Block a user