"use client"; import { useEffect, useLayoutEffect, useRef, useState, type MouseEvent, } from "react"; import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; import { useMutation } from "convex/react"; import { ExternalLink, ImageIcon } from "lucide-react"; import BaseNodeWrapper from "./base-node-wrapper"; import { AssetBrowserPanel, type AssetBrowserSessionState, } from "@/components/canvas/asset-browser-panel"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { computeMediaNodeSize, resolveMediaAspectRatio } from "@/lib/canvas-utils"; type AssetNodeData = { assetId?: number; assetType?: "photo" | "vector" | "icon"; title?: string; previewUrl?: string; intrinsicWidth?: number; intrinsicHeight?: number; url?: string; sourceUrl?: string; license?: "freemium" | "premium"; authorName?: string; orientation?: string; canvasId?: string; _status?: string; _statusMessage?: string; }; export type AssetNodeType = Node; export default function AssetNode({ id, data, selected, width, height }: NodeProps) { const [panelOpen, setPanelOpen] = useState(false); const [handleTop, setHandleTop] = useState(undefined); const [browserState, setBrowserState] = useState({ term: "", assetType: "photo", results: [], page: 1, totalPages: 1, }); const resizeNode = useMutation(api.nodes.resize); const contentRef = useRef(null); const mediaRef = useRef(null); const hasAsset = typeof data.assetId === "number"; const previewUrl = data.url ?? data.previewUrl; const aspectRatio = resolveMediaAspectRatio( data.intrinsicWidth, data.intrinsicHeight, data.orientation, ); useEffect(() => { if (!hasAsset) return; const targetSize = computeMediaNodeSize("asset", { intrinsicWidth: data.intrinsicWidth, intrinsicHeight: data.intrinsicHeight, orientation: data.orientation, }); if (width === targetSize.width && height === targetSize.height) { return; } void resizeNode({ nodeId: id as Id<"nodes">, width: targetSize.width, height: targetSize.height, }); }, [ data.intrinsicHeight, data.intrinsicWidth, data.orientation, hasAsset, height, id, resizeNode, width, ]); useLayoutEffect(() => { if (!hasAsset || !contentRef.current || !mediaRef.current) return; const contentEl = contentRef.current; const mediaEl = mediaRef.current; let frameId: number | undefined; const updateHandleTop = () => { if (frameId !== undefined) { cancelAnimationFrame(frameId); } frameId = requestAnimationFrame(() => { const contentRect = contentEl.getBoundingClientRect(); const mediaRect = mediaEl.getBoundingClientRect(); const nextTop = mediaRect.top - contentRect.top + mediaRect.height / 2; setHandleTop(nextTop); }); }; updateHandleTop(); const observer = new ResizeObserver(updateHandleTop); observer.observe(contentEl); observer.observe(mediaEl); return () => { observer.disconnect(); if (frameId !== undefined) { cancelAnimationFrame(frameId); } }; }, [aspectRatio, hasAsset]); const stopNodeClickPropagation = (event: MouseEvent) => { event.stopPropagation(); }; return (
Asset
{hasAsset && previewUrl ? (
{/* eslint-disable-next-line @next/next/no-img-element */} {data.title {data.assetType ?? "asset"} {data.license ? ( {data.license} ) : null}

{data.title ?? "Untitled"}

by {data.authorName ?? "Freepik"} {data.sourceUrl ? ( freepik.com ) : null}
) : (

No asset selected

Browse millions of Freepik resources

)}
{panelOpen && data.canvasId ? ( setPanelOpen(false)} /> ) : null}
); }