/* 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

)}
); } // ─── 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 (
{alt {/* 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 ? ( Video thumbnail ) : (
)}

{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 }) => (
)} > {showHeader && } {blocks.length === 0 ? (

{empty}

) : (
{blocks.map((block) => (
))}
)}
); } export function PreviewChrome({ title, children, showChrome = true }) { if (!showChrome) { return <>{children}; } return (
{title ?? "Lesson Preview"}
{children}
); }