course,tasklist,task and completed validation

Signed-off-by: rgrgogu <obsequio.rus@gmail.com>
This commit is contained in:
rgrgogu
2026-07-17 13:04:40 +08:00
parent 5b9f174718
commit 2e9ab5786c
22 changed files with 1345 additions and 117 deletions
@@ -135,11 +135,12 @@ export function PreviewVideo({ url, thumb }) {
// AudioBlock themselves) attaches the block's own id/type to each call, since a lesson
// can have several blocks of the same type and watch_video/listen_audio need to know
// which specific one just reported progress.
export function PreviewBlock({ block, onWatchProgress }) {
export function PreviewBlock({ block, onWatchProgress, resumeMap, antiSkipEnabled }) {
const { id, type, content } = block;
const withBlockMeta = onWatchProgress
? (percent) => onWatchProgress(percent, { blockId: id, blockType: type })
: undefined;
const resumePercent = resumeMap?.[id];
switch (type) {
case "text":
@@ -149,11 +150,11 @@ export function PreviewBlock({ block, onWatchProgress }) {
case "text-image":
return <TextImageBlock blockId={id} content={content} readOnly />;
case "video":
return <VideoBlock content={content} readOnly onWatchProgress={withBlockMeta} />;
return <VideoBlock content={content} readOnly onWatchProgress={withBlockMeta} resumePercent={resumePercent} antiSkipEnabled={antiSkipEnabled} />;
case "text-video":
return <TextVideoBlock blockId={id} content={content} readOnly onWatchProgress={withBlockMeta} />;
return <TextVideoBlock blockId={id} content={content} readOnly onWatchProgress={withBlockMeta} resumePercent={resumePercent} antiSkipEnabled={antiSkipEnabled} />;
case "audio":
return <AudioBlock content={content} onWatchProgress={withBlockMeta} />;
return <AudioBlock content={content} onWatchProgress={withBlockMeta} resumePercent={resumePercent} antiSkipEnabled={antiSkipEnabled} />;
case "code":
return <CodeBlock content={content} />;
case "markdown":
@@ -167,7 +168,13 @@ export function PreviewBlock({ block, onWatchProgress }) {
// PhotoProvider wraps ALL blocks so images across the whole lesson share
// one lightbox session — users can swipe between them naturally.
// Only these completion-requirement types gate video/audio behind the anti-skip
// seek-cap — everything else (or no requirement configured) allows free seeking.
const WATCH_TYPE_REQUIREMENTS = ["watch_percent", "watch_video", "listen_audio"];
export function PreviewContent({ lesson, blocks, empty = "No content yet.", showHeader = true, onWatchProgress }) {
const resumeMap = lesson?.resume_positions;
const antiSkipEnabled = WATCH_TYPE_REQUIREMENTS.includes(lesson?.completion?.type);
return (
<PhotoProvider
speed={() => 300}
@@ -194,7 +201,7 @@ export function PreviewContent({ lesson, blocks, empty = "No content yet.", show
<div className="space-y-4 sm:space-y-5">
{blocks.map((block) => (
<div key={block.id}>
<PreviewBlock block={block} onWatchProgress={onWatchProgress} />
<PreviewBlock block={block} onWatchProgress={onWatchProgress} resumeMap={resumeMap} antiSkipEnabled={antiSkipEnabled} />
</div>
))}
</div>