/* A similar strategy to BlockList.jsx so this will applies
to Client side */
import { Eye, ImageIcon, VideoIcon, ZoomIn } from "lucide-react";
import { PhotoProvider, PhotoView } from "react-photo-view";
import { VideoBlock } from "@/components/generic/Blocks/Client/VideoBlock";
import { TextVideoBlock } from "@/components/generic/Blocks/Client/TextVideoBlock";
import { TextImageBlock } from "@/components/generic/Blocks/Client/TextImageBlock";
import { ImageBlock } from "@/components/generic/Blocks/Client/ImageBlock";
import { TextBlock } from "@/components/generic/Blocks/Client/TextBlock";
import { AudioBlock } from "@/components/generic/Blocks/Client/AudioBlock";
import { CodeBlock } from "@/components/generic/Blocks/Client/CodeBlock";
import { MarkdownBlock } from "@/components/generic/Blocks/Client/MarkdownBlock";
export function LessonHeader({ lesson }) {
if (!lesson) return null;
return (
{lesson.title}
{lesson.description && (
{lesson.description}
)}
{lesson.objectives?.length > 0 && (
Objective
{lesson.objectives.map((o) => (
{o.text}
))}
)}
);
}
// ─── Zoomable image wrapper ───────────────────────────────────────────────────
// Must be rendered inside a . Shows a subtle zoom hint on hover.
export function ZoomableImage({ url, alt }) {
if (!url) return null;
return (
{/* Zoom hint badge — fades in on hover */}
Zoom
);
}
export function PreviewImage({ url, alt }) {
if (!url) {
return (
No image
);
}
return ;
}
export function PreviewVideo({ url, thumb }) {
if (!url) {
return (
No video
);
}
return (
{thumb ? (
) : (
)}
{url}
);
}
// onWatchProgress(percent, { blockId, blockType }) — only meaningful for video/audio
// blocks; optional, undefined in admin preview mode (only the client reader passes it,
// for watch_percent/watch_video/listen_audio tracking). PreviewBlock (not VideoBlock/
// 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, 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":
return ;
case "image":
return ;
case "text-image":
return ;
case "video":
return ;
case "text-video":
return ;
case "audio":
return ;
case "code":
return ;
case "markdown":
return ;
default:
return null;
}
}
// ─── PreviewContent ───────────────────────────────────────────────────────────
// 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 (
300}
easing={(type) => (type === 2 ? "cubic-bezier(0.36, 0, 0.66, -0.56)" : "cubic-bezier(0.34, 1.56, 0.64, 1)")}
toolbarRender={({ onScale, scale, rotate, onRotate }) => (