"use client"; import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent, } from "react"; import { Handle, Position, useStore, type Node, type NodeProps } from "@xyflow/react"; import { ExternalLink, ImageIcon } from "lucide-react"; import BaseNodeWrapper from "./base-node-wrapper"; import { AssetBrowserPanel, useAssetBrowserTarget, type AssetBrowserSessionState, } from "@/components/canvas/asset-browser-panel"; import type { Id } from "@/convex/_generated/dataModel"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { resolveMediaAspectRatio } from "@/lib/canvas-utils"; import { useCanvasSync } from "@/components/canvas/canvas-sync-context"; 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 { targetNodeId, openForNode, close: closeAssetBrowser } = useAssetBrowserTarget(); const panelOpen = targetNodeId === id; const [loadedPreviewUrl, setLoadedPreviewUrl] = useState(null); const [failedPreviewUrl, setFailedPreviewUrl] = useState(null); const [browserState, setBrowserState] = useState({ term: "", assetType: "photo", results: [], page: 1, totalPages: 1, }); const { queueNodeResize } = useCanvasSync(); 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 openAssetBrowser = useCallback(() => { setBrowserState((s) => linkedSearchTerm ? { ...s, term: linkedSearchTerm, results: [], page: 1, totalPages: 1 } : s, ); openForNode(id); }, [id, linkedSearchTerm, openForNode]); const hasAsset = typeof data.assetId === "number"; const previewUrl = data.url ?? data.previewUrl; const isPreviewLoading = Boolean( previewUrl && previewUrl !== loadedPreviewUrl && previewUrl !== failedPreviewUrl, ); const previewLoadError = Boolean(previewUrl && previewUrl === failedPreviewUrl); const hasAutoSizedRef = useRef(false); useEffect(() => { if (!hasAsset) return; if (hasAutoSizedRef.current) return; const targetAspectRatio = resolveMediaAspectRatio( data.intrinsicWidth, data.intrinsicHeight, data.orientation, ); const minimumNodeHeight = 208; const baseNodeWidth = 260; const targetWidth = Math.max(baseNodeWidth, Math.round(minimumNodeHeight * targetAspectRatio)); const targetHeight = Math.round(targetWidth / targetAspectRatio); const targetSize = { width: targetWidth, height: targetHeight, }; const currentWidth = typeof width === "number" ? width : 0; const currentHeight = typeof height === "number" ? height : 0; const hasMeasuredSize = currentWidth > 0 && currentHeight > 0; if (!hasMeasuredSize) { return; } const isAtTargetSize = currentWidth === targetSize.width && currentHeight === targetSize.height; const isAtDefaultSeedSize = currentWidth === 260 && currentHeight === 240; const shouldRunInitialAutoSize = isAtDefaultSeedSize && !isAtTargetSize; if (!shouldRunInitialAutoSize) { hasAutoSizedRef.current = true; return; } hasAutoSizedRef.current = true; void queueNodeResize({ nodeId: id as Id<"nodes">, width: targetSize.width, height: targetSize.height, }); }, [ data.intrinsicHeight, data.intrinsicWidth, data.orientation, hasAsset, height, id, queueNodeResize, width, ]); const stopNodeClickPropagation = (event: MouseEvent) => { event.stopPropagation(); }; const showPreview = Boolean(hasAsset && previewUrl); return (
FreePik
{showPreview ? ( <>
{isPreviewLoading ? (
Loading preview...
) : null} {previewLoadError ? (
Preview unavailable
) : null} {/* eslint-disable-next-line @next/next/no-img-element */} {data.title { setLoadedPreviewUrl(previewUrl ?? null); setFailedPreviewUrl((current) => current === (previewUrl ?? null) ? null : current, ); }} onError={() => { setFailedPreviewUrl(previewUrl ?? null); }} /> {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 ? ( ) : null}
); }