"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Handle, Position, useStore, type NodeProps } from "@xyflow/react"; import { useAction, useMutation } from "convex/react"; import { Play } from "lucide-react"; import BaseNodeWrapper from "./base-node-wrapper"; import { VideoBrowserPanel, type VideoBrowserSessionState, } from "@/components/canvas/video-browser-panel"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; type VideoNodeData = { canvasId?: string; pexelsId?: number; mp4Url?: string; thumbnailUrl?: string; width?: number; height?: number; duration?: number; attribution?: { userName: string; userUrl: string; videoUrl: string; }; }; function formatDuration(seconds: number): string { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${s.toString().padStart(2, "0")}`; } export default function VideoNode({ id, data, selected, width, height, }: NodeProps) { const d = data as VideoNodeData; const [panelOpen, setPanelOpen] = useState(false); const [browserState, setBrowserState] = useState({ term: "", orientation: "", durationFilter: "all", results: [], page: 1, totalPages: 1, }); const resizeNode = useMutation(api.nodes.resize); const updateData = useMutation(api.nodes.updateData); const refreshPexelsPlayback = useAction(api.pexels.getVideoByPexelsId); const edges = useStore((s) => s.edges); const nodes = useStore((s) => s.nodes); const linkedSearchTerm = useMemo(() => { const incoming = edges.filter((e) => e.target === id); for (const edge of incoming) { const sourceNode = nodes.find((n) => n.id === edge.source); if (sourceNode?.type !== "text") continue; const content = (sourceNode.data as { content?: string }).content; if (typeof content === "string" && content.trim().length > 0) { return content.trim(); } } return ""; }, [edges, id, nodes]); const openVideoBrowser = useCallback(() => { setBrowserState((s) => linkedSearchTerm ? { ...s, term: linkedSearchTerm, results: [], page: 1, totalPages: 1 } : s, ); setPanelOpen(true); }, [linkedSearchTerm]); const hasVideo = Boolean(d.mp4Url && d.thumbnailUrl); const hasAutoSizedRef = useRef(false); const playbackRefreshAttempted = useRef(false); useEffect(() => { playbackRefreshAttempted.current = false; }, [d.mp4Url]); const handleVideoError = useCallback(() => { const pexelsId = d.pexelsId; if (pexelsId == null || playbackRefreshAttempted.current) return; playbackRefreshAttempted.current = true; void (async () => { try { const fresh = await refreshPexelsPlayback({ pexelsId }); await updateData({ nodeId: id as Id<"nodes">, data: { ...d, mp4Url: fresh.mp4Url, width: fresh.width, height: fresh.height, duration: fresh.duration, }, }); } catch { playbackRefreshAttempted.current = false; } })(); }, [d, id, refreshPexelsPlayback, updateData]); useEffect(() => { if (!hasVideo) return; if (hasAutoSizedRef.current) return; const w = d.width; const h = d.height; if (typeof w !== "number" || typeof h !== "number" || w <= 0 || h <= 0) return; const currentWidth = typeof width === "number" ? width : 0; const currentHeight = typeof height === "number" ? height : 0; if (currentWidth <= 0 || currentHeight <= 0) return; if (currentWidth !== 320 || currentHeight !== 180) { hasAutoSizedRef.current = true; return; } hasAutoSizedRef.current = true; const aspectRatio = w / h; const targetWidth = 320; const targetHeight = Math.round(targetWidth / aspectRatio); void resizeNode({ nodeId: id as Id<"nodes">, width: targetWidth, height: targetHeight, }); }, [d.width, d.height, hasVideo, height, id, resizeNode, width]); const showPreview = hasVideo && d.thumbnailUrl; const playbackSrc = d.mp4Url != null && d.mp4Url.length > 0 ? `/api/pexels-video?u=${encodeURIComponent(d.mp4Url)}` : undefined; return (
{/* Header */}
Pexels
{/* Content: flex-1 + min-h-0 keeps media inside the node; avoid aspect-ratio here (grid overflow). */} {showPreview ? ( <>
{/* Attribution */} {d.attribution ? (
by {d.attribution.userName} e.stopPropagation()} > Pexels
) : (
)} ) : (

No video selected

Browse free stock videos from Pexels

)}
{/* Video browser modal */} {panelOpen && d.canvasId ? ( setPanelOpen(false)} /> ) : null} ); }