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:
@@ -90,7 +90,7 @@ const QUESTION_VALIDATION_RULES: Record<number, Array<(question: QuestionRuleInp
|
||||
(question) => {
|
||||
const settings = question.settings;
|
||||
const template = String(settings?.template ?? '');
|
||||
const blankCount = Math.max(0, template.split('___').length - 1);
|
||||
const blankCount = (template.match(/_{3,}/g) ?? []).length;
|
||||
if (blankCount < 1) {
|
||||
return 'Word bank questions need at least one ___ blank in the template';
|
||||
}
|
||||
@@ -224,7 +224,9 @@ export const ZExerciseUpdate = z.object({
|
||||
slug: ZSlug.optional(),
|
||||
questions: z.array(ZExerciseUpdateQuestion).optional(),
|
||||
sections: z.array(ZExerciseSection).optional(),
|
||||
sectionDisplayMode: z.enum(['one_question', 'all_questions']).optional()
|
||||
sectionDisplayMode: z.enum(['one_question', 'all_questions']).optional(),
|
||||
completionPolicy: z.enum(['submitted', 'passed']).optional(),
|
||||
passThreshold: z.number().int().min(0).max(100).optional()
|
||||
});
|
||||
export type TExerciseUpdate = z.infer<typeof ZExerciseUpdate>;
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ export const ZLessonUpdate = z.object({
|
||||
public: z.boolean().optional(),
|
||||
slug: ZSlug.optional(),
|
||||
isComplete: z.boolean().optional(),
|
||||
completionPolicy: z.enum(['manual', 'video_watch', 'none']).optional(),
|
||||
videoWatchThreshold: z.number().int().min(1).max(100).optional(),
|
||||
videoUrl: z.url().optional(),
|
||||
slideUrl: z.url().optional(),
|
||||
videos: z
|
||||
@@ -38,6 +40,7 @@ export const ZLessonUpdate = z.object({
|
||||
link: z.string(),
|
||||
key: z.string().optional(),
|
||||
assetId: z.string().uuid().optional(),
|
||||
watchEnforced: z.boolean().optional(),
|
||||
fileName: z.string().optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional()
|
||||
})
|
||||
@@ -128,3 +131,17 @@ export const ZLessonCompletionUpdate = z.object({
|
||||
isComplete: z.boolean()
|
||||
});
|
||||
export type TLessonCompletionUpdate = z.infer<typeof ZLessonCompletionUpdate>;
|
||||
|
||||
export const ZUpdateLessonWatchProgress = z.object({
|
||||
positionSeconds: z.number().min(0),
|
||||
playedDeltaSeconds: z
|
||||
.number()
|
||||
.transform((value) => Math.round(value))
|
||||
.pipe(z.number().min(0).max(120)),
|
||||
durationSeconds: z
|
||||
.number()
|
||||
.transform((value) => Math.round(value))
|
||||
.pipe(z.int().min(1)),
|
||||
assetId: z.string().optional()
|
||||
});
|
||||
export type TUpdateLessonWatchProgress = z.infer<typeof ZUpdateLessonWatchProgress>;
|
||||
|
||||
Reference in New Issue
Block a user