mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -1,29 +1,40 @@
|
||||
import { Eye, ImageIcon, VideoIcon } from "lucide-react";
|
||||
import { WYSIWYG_STYLES } from "@/components/generic/Blocks/TextBlock";
|
||||
/* A similar strategy to BlockList.jsx so this will applies
|
||||
to Client side */
|
||||
|
||||
import { Eye, ImageIcon, VideoIcon, ZoomIn } from "lucide-react";
|
||||
import { WYSIWYG_STYLES } from "@/components/generic/Blocks/Admin/TextBlock";
|
||||
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 (
|
||||
<div className="space-y-4 pb-2">
|
||||
<div className="space-y-3 pb-2 sm:space-y-4">
|
||||
<div>
|
||||
<h1 style={{ fontSize: "1.875rem", fontWeight: 700, lineHeight: 1.2, margin: "0 0 0.4rem 0" }}>
|
||||
<h1 className="text-2xl font-bold leading-tight mb-1 sm:text-3xl sm:leading-[1.2]">
|
||||
{lesson.title}
|
||||
</h1>
|
||||
{lesson.description && (
|
||||
<p style={{ margin: "0.2rem 0", lineHeight: 1.75, textAlign: "justify" }}
|
||||
className="text-muted-foreground">
|
||||
<p className="mt-1 leading-relaxed text-sm text-muted-foreground sm:text-base sm:leading-[1.75] sm:text-justify">
|
||||
{lesson.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{lesson.objectives?.length > 0 && (
|
||||
<div className="rounded-lg border p-4 space-y-3">
|
||||
<div className="rounded-lg border p-3 space-y-2 sm:p-4 sm:space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-md bg-green-100 flex items-center justify-center shrink-0">
|
||||
<div className="h-7 w-7 rounded-md bg-green-100 flex items-center justify-center shrink-0 sm:h-8 sm:w-8">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 text-green-600"
|
||||
className="h-3.5 w-3.5 text-green-600 sm:h-4 sm:w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -36,11 +47,11 @@ export function LessonHeader({ lesson }) {
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm font-semibold">Objective</p>
|
||||
<p className="text-xs font-semibold sm:text-sm">Objective</p>
|
||||
</div>
|
||||
<ul className="space-y-1.5 list-disc list-inside">
|
||||
<ul className="space-y-1 list-disc list-inside sm:space-y-1.5">
|
||||
{lesson.objectives.map((o) => (
|
||||
<li key={o.objective_id} className="text-sm text-muted-foreground">
|
||||
<li key={o.objective_id} className="text-xs text-muted-foreground sm:text-sm">
|
||||
{o.text}
|
||||
</li>
|
||||
))}
|
||||
@@ -51,6 +62,32 @@ export function LessonHeader({ lesson }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Zoomable image wrapper ───────────────────────────────────────────────────
|
||||
// Must be rendered inside a <PhotoProvider>. Shows a subtle zoom hint on hover.
|
||||
|
||||
export function ZoomableImage({ url, alt }) {
|
||||
if (!url) return null;
|
||||
return (
|
||||
<PhotoView src={url}>
|
||||
<div className="relative group cursor-zoom-in">
|
||||
<img
|
||||
src={url}
|
||||
alt={alt ?? ""}
|
||||
className="w-full rounded-md object-cover aspect-video"
|
||||
draggable={false}
|
||||
/>
|
||||
{/* Zoom hint badge — fades in on hover */}
|
||||
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||
<div className="flex items-center gap-1 bg-secondary text-sm px-2 py-1 rounded-full border">
|
||||
<ZoomIn className="size-4" />
|
||||
<span>Zoom</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PhotoView>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewImage({ url, alt }) {
|
||||
if (!url) {
|
||||
return (
|
||||
@@ -60,9 +97,7 @@ export function PreviewImage({ url, alt }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<img src={url} alt={alt ?? ""} className="w-full rounded-md object-cover aspect-video" />
|
||||
);
|
||||
return <ZoomableImage url={url} alt={alt} />;
|
||||
}
|
||||
|
||||
export function PreviewVideo({ url, thumb }) {
|
||||
@@ -80,129 +115,107 @@ export function PreviewVideo({ url, thumb }) {
|
||||
<img src={thumb} alt="Video thumbnail" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<VideoIcon className="h-10 w-10 text-muted-foreground/40" />
|
||||
<VideoIcon className="h-8 w-8 text-muted-foreground/40 sm:h-10 sm:w-10" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="h-10 w-10 rounded-full bg-black/50 flex items-center justify-center">
|
||||
<VideoIcon className="h-5 w-5 text-white" />
|
||||
<div className="h-8 w-8 rounded-full bg-black/50 flex items-center justify-center sm:h-10 sm:w-10">
|
||||
<VideoIcon className="h-4 w-4 text-white sm:h-5 sm:w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-0 inset-x-0 bg-black/60 px-2 py-1">
|
||||
<p className="text-white text-[10px] truncate">{url}</p>
|
||||
<p className="text-white text-[9px] truncate sm:text-[10px]">{url}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewBlock({ block }) {
|
||||
const { type, content } = block;
|
||||
const { id, type, content } = block;
|
||||
|
||||
if (type === "text") {
|
||||
if (!content.body) {
|
||||
return <div className="text-xs text-muted-foreground italic py-2">Empty text block</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="wysiwyg-preview text-sm"
|
||||
dangerouslySetInnerHTML={{ __html: content.body }}
|
||||
/>
|
||||
);
|
||||
switch (type) {
|
||||
case "text":
|
||||
return <TextBlock blockId={id} content={content} readOnly />;
|
||||
case "image":
|
||||
return <ImageBlock content={content} readOnly />;
|
||||
case "text-image":
|
||||
return <TextImageBlock blockId={id} content={content} readOnly />;
|
||||
case "video":
|
||||
return <VideoBlock content={content} readOnly />;
|
||||
case "text-video":
|
||||
return <TextVideoBlock blockId={id} content={content} readOnly />;
|
||||
case "audio":
|
||||
return <AudioBlock content={content} />;
|
||||
case "code":
|
||||
return <CodeBlock content={content} />;
|
||||
case "markdown":
|
||||
return <MarkdownBlock content={content} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === "image") {
|
||||
if (!content.url) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-24 rounded-lg border border-dashed text-xs text-muted-foreground bg-muted/20 gap-1.5">
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
No image selected
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<figure>
|
||||
<img src={content.url} alt={content.alt ?? ""} className="w-full rounded-md object-cover" />
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "text-image") {
|
||||
const imgLeft = content.image_position === "left";
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
{imgLeft && <PreviewImage url={content.url} alt={content.alt} />}
|
||||
<div
|
||||
className="wysiwyg-preview text-sm"
|
||||
dangerouslySetInnerHTML={{ __html: content.body || "<p class='text-muted-foreground italic'>Empty text</p>" }}
|
||||
/>
|
||||
{!imgLeft && <PreviewImage url={content.url} alt={content.alt} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "video") {
|
||||
if (!content.url) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-24 rounded-lg border border-dashed text-xs text-muted-foreground bg-muted/20 gap-1.5">
|
||||
<VideoIcon className="h-4 w-4" />
|
||||
No video selected
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <PreviewVideo url={content.url} thumb={content.thumbnail_url} />;
|
||||
}
|
||||
|
||||
if (type === "text-video") {
|
||||
const vidLeft = content.video_position === "left";
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
{vidLeft && <PreviewVideo url={content.url} thumb={content.thumbnail_url} />}
|
||||
<div
|
||||
className="wysiwyg-preview text-sm"
|
||||
dangerouslySetInnerHTML={{ __html: content.body || "<p class='text-muted-foreground italic'>Empty text</p>" }}
|
||||
/>
|
||||
{!vidLeft && <PreviewVideo url={content.url} thumb={content.thumbnail_url} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── PreviewContent ───────────────────────────────────────────────────────────
|
||||
// PhotoProvider wraps ALL blocks so images across the whole lesson share
|
||||
// one lightbox session — users can swipe between them naturally.
|
||||
|
||||
export function PreviewContent({ lesson, blocks, empty = "No content yet." }) {
|
||||
return (
|
||||
<>
|
||||
<PhotoProvider
|
||||
speed={() => 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 }) => (
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<button
|
||||
onClick={() => onScale(scale + 0.5)}
|
||||
className="text-white/80 hover:text-white transition-colors"
|
||||
title="Zoom in"
|
||||
>
|
||||
<ZoomIn className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<style>{WYSIWYG_STYLES}</style>
|
||||
<LessonHeader lesson={lesson} />
|
||||
{blocks.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-16 text-sm text-muted-foreground">
|
||||
<Eye className="h-8 w-8 opacity-20" />
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-10 text-sm text-muted-foreground sm:py-16">
|
||||
<Eye className="h-6 w-6 opacity-20 sm:h-8 sm:w-8" />
|
||||
<p>{empty}</p>
|
||||
</div>
|
||||
) : (
|
||||
blocks.map((block) => (
|
||||
<div key={block.id}>
|
||||
<PreviewBlock block={block} />
|
||||
</div>
|
||||
))
|
||||
<div className="space-y-4 sm:space-y-5">
|
||||
{blocks.map((block) => (
|
||||
<div key={block.id}>
|
||||
<PreviewBlock block={block} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</PhotoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewChrome({ title, children }) {
|
||||
export function PreviewChrome({ title, children, showChrome = true }) {
|
||||
if (!showChrome) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
|
||||
<style>{WYSIWYG_STYLES}</style>
|
||||
<div className="flex items-center gap-1.5 px-3 py-2 bg-muted/60 border-b">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-red-400" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-yellow-400" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-green-400" />
|
||||
<div className="flex-1 mx-3 h-5 rounded bg-background/60 border text-[10px] flex items-center px-2 text-muted-foreground/60 truncate">
|
||||
<span className="hidden h-2.5 w-2.5 rounded-full bg-red-400 sm:inline-block" />
|
||||
<span className="hidden h-2.5 w-2.5 rounded-full bg-yellow-400 sm:inline-block" />
|
||||
<span className="hidden h-2.5 w-2.5 rounded-full bg-green-400 sm:inline-block" />
|
||||
<div className="flex-1 sm:mx-3 h-5 rounded bg-background/60 border text-[10px]
|
||||
flex items-center px-2 text-muted-foreground/60 truncate">
|
||||
{title ?? "Lesson Preview"}
|
||||
</div>
|
||||
<Eye className="h-3.5 w-3.5 text-muted-foreground/60" />
|
||||
<Eye className="h-3.5 w-3.5 text-muted-foreground/60 shrink-0" />
|
||||
</div>
|
||||
<div className="p-3 sm:p-5">
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user