"use client"; import { useEffect, 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 { 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 [loadedPreviewUrl, setLoadedPreviewUrl] = useState(null); const [failedPreviewUrl, setFailedPreviewUrl] = useState(null); const [browserState, setBrowserState] = useState({ term: "", assetType: "photo", results: [], page: 1, totalPages: 1, }); const resizeNode = useMutation(api.nodes.resize); 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 resizeNode({ nodeId: id as Id<"nodes">, width: targetSize.width, height: targetSize.height, }); }, [ data.intrinsicHeight, data.intrinsicWidth, data.orientation, hasAsset, height, id, resizeNode, width, ]); const stopNodeClickPropagation = (event: MouseEvent) => { event.stopPropagation(); }; const showPreview = Boolean(hasAsset && previewUrl); return (
Asset
{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 ? ( setPanelOpen(false)} /> ) : null}
); }