Files
starr-philproperties/apps/web/src/components/generic/Blocks/Admin/TextVideoBlock.jsx
T
kennethobsequio b37218f90e merge new_starr_app@qas history into apps/web
git-subtree-dir: apps/web
git-subtree-mainline: eaa6d2276a
git-subtree-split: 459af9d46a
2026-08-24 12:21:16 +08:00

124 lines
4.7 KiB
React

// components/generic/CMS/Blocks/TextVideoBlock.jsx
import { useState } from "react";
import { VideoIcon } from "lucide-react";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RichTextEditor } from "./TextBlock"; // reuse the shared editor
import { AssetPickerSheet } from "../../AssetPickerSheet";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { MediaFallback } from "@/components/generic/MediaFallback";
export function TextVideoBlock({ content, onUpdate, blockId, readOnly = false }) {
const [pickerOpen, setPickerOpen] = useState(false);
// content.thumbnail_url is a presigned S3 URL captured at pick time for
// S3 assets — it expires. Re-resolve through media.util.js instead of
// trusting the persisted value, mirroring VideoBlock.jsx.
const { thumbnailUrl, loading } = useAssetPreviewSrc(
{ asset_id: content?.asset_id, storage_provider: content?.storage_provider, file_url: content?.url, thumbnail_url: content?.thumbnail_url },
{ scope: "admin" },
);
const thumb = thumbnailUrl ?? content.thumbnail_url ?? null;
return (
<div className="space-y-4">
{/* ── Video position ── */}
<div className="space-y-1.5 max-w-[200px]">
<Label>Video Position</Label>
<Select
value={content.video_position ?? "right"}
onValueChange={(v) => onUpdate({ ...content, video_position: v })}
>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* ── Text side — WYSIWYG ── */}
<div className="space-y-1.5">
<Label>Content</Label>
<RichTextEditor
blockId={`${blockId}_text`}
value={content.body ?? ""}
onChange={(html) => onUpdate({ ...content, body: html })}
/>
</div>
{/* ── Video side ── */}
<div className={[
"space-y-1.5",
content.video_position === "left" ? "md:order-first" : "",
].join(" ")}>
<Label>Video</Label>
{loading ? (
<MediaFallback className="w-full aspect-video rounded-lg" />
) : content.asset_id ? (
<div
className="relative rounded-lg overflow-hidden cursor-pointer group"
onClick={() => setPickerOpen(true)}
>
{thumb ? (
<img
src={thumb}
alt="Video thumbnail"
className="w-full aspect-video object-cover"
/>
) : (
<div className="w-full aspect-video bg-muted flex items-center justify-center">
<VideoIcon className="h-10 w-10 text-muted-foreground/50" />
</div>
)}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<p className="text-white text-sm font-medium">Change Video</p>
</div>
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="h-12 w-12 rounded-full bg-black/50 flex items-center justify-center">
<VideoIcon className="h-5 w-5 text-white" />
</div>
</div>
</div>
) : (
<button
type="button"
onClick={() => setPickerOpen(true)}
className="w-full aspect-video rounded-lg border-2 border-dashed border-muted-foreground/30 hover:border-primary/50 hover:bg-muted/20 transition-colors flex flex-col items-center justify-center gap-2"
>
<VideoIcon className="h-8 w-8 text-muted-foreground/50" />
<p className="text-sm text-muted-foreground">Click to select a video</p>
</button>
)}
</div>
</div>
{!readOnly && (
<AssetPickerSheet
open={pickerOpen}
onOpenChange={setPickerOpen}
fileType="video"
onSelect={(asset) => onUpdate({
...content,
asset_id: asset.asset_id,
storage_provider: asset.storage_provider ?? null,
url: asset.file_url,
thumbnail_url: asset.thumbnail_url ?? null,
duration_seconds: Number(asset.duration) || 0,
})}
/>
)}
</div>
);
}