UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

311 lines (310 loc) 17.5 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /** * ArtifactDisplay Component * * Universal content renderer for blockchain artifacts (ordinals, inscriptions, files) * Supports images, videos, audio, HTML, text, JSON, SVG, and more * Uses BitcoinImage component for blockchain URL processing */ import { CopyIcon, CrossCircledIcon, DownloadIcon, ExternalLinkIcon, InfoCircledIcon, } from "@radix-ui/react-icons"; import { ImageProtocols } from "bitcoin-image"; import { Loader2 } from "lucide-react"; import React, { useState, useMemo, useCallback } from "react"; import { cn } from "../../lib/utils.js"; import { BitcoinImage } from "../ui-components/BitcoinImage.js"; import { Badge } from "../ui/badge.js"; import { Button } from "../ui/button.js"; import { Card, CardContent } from "../ui/card.js"; import { Dialog, DialogContent, DialogOverlay } from "../ui/dialog.js"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../ui/tooltip.js"; // Initialize bitcoin-image protocols const imageProtocols = new ImageProtocols(); // Types export var ArtifactType; (function (ArtifactType) { ArtifactType["Image"] = "image"; ArtifactType["Video"] = "video"; ArtifactType["Audio"] = "audio"; ArtifactType["HTML"] = "html"; ArtifactType["Text"] = "text"; ArtifactType["JSON"] = "json"; ArtifactType["SVG"] = "svg"; ArtifactType["PDF"] = "pdf"; ArtifactType["Model"] = "model"; ArtifactType["Unknown"] = "unknown"; })(ArtifactType || (ArtifactType = {})); // Shared error display component const ErrorDisplay = ({ message, size, maxWidth, className }) => (_jsxs("div", { className: cn("flex flex-col items-center justify-center bg-muted border border-dashed border-muted-foreground/30 rounded-md", className), style: { width: size || maxWidth || 300, height: size || 200, }, children: [_jsx(CrossCircledIcon, { className: "w-6 h-6 text-muted-foreground" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground text-center", children: message })] })); // Custom hook for fetching content const useContentFetch = (src, parser) => { const [content, setContent] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); React.useEffect(() => { let mounted = true; const fetchContent = async () => { try { setLoading(true); setError(""); const response = await fetch(src); const data = parser ? await parser(response) : await response.text(); if (mounted) { setContent(data); setLoading(false); } } catch (err) { if (mounted) { setError(err instanceof Error ? err.message : "Failed to load content"); setLoading(false); } } }; fetchContent(); return () => { mounted = false; }; }, [src, parser]); return { content, loading, error }; }; // Content type detection const getArtifactTypeFromContentType = (contentType) => { if (contentType.startsWith("image/")) { return contentType === "image/svg+xml" ? ArtifactType.SVG : ArtifactType.Image; } if (contentType.startsWith("video/")) return ArtifactType.Video; if (contentType.startsWith("audio/")) return ArtifactType.Audio; if (contentType === "text/html") return ArtifactType.HTML; if (contentType === "application/json") return ArtifactType.JSON; if (contentType.startsWith("text/")) return ArtifactType.Text; if (contentType === "application/pdf") return ArtifactType.PDF; if (contentType.startsWith("model/")) return ArtifactType.Model; return ArtifactType.Unknown; }; // Image component using BitcoinImage const ArtifactImage = ({ src, alt, size, maxWidth, allowZoom, className, onClick }) => { const errorFallback = (_jsx(ErrorDisplay, { message: "Failed to load image", size: size, maxWidth: maxWidth, className: className })); return (_jsxs("div", { className: className, style: { position: "relative" }, children: [_jsx(BitcoinImage, { src: src, alt: alt, style: { maxWidth: maxWidth || "100%", width: size ? `${size}px` : "auto", height: size ? `${size}px` : "auto", objectFit: "contain", cursor: allowZoom || onClick ? "pointer" : "default", }, fallback: errorFallback, timeout: 10000 }), (allowZoom || onClick) && (_jsx("div", { style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, cursor: "pointer", }, onClick: onClick, onKeyDown: (e) => { if ((e.key === "Enter" || e.key === " ") && onClick) { e.preventDefault(); onClick(); } }, tabIndex: onClick ? 0 : -1, role: onClick ? "button" : undefined }))] })); }; // Video component const ArtifactVideo = ({ src, maxWidth, className }) => (_jsxs("video", { src: src, controls: true, style: { maxWidth: maxWidth || "100%", width: "100%" }, className: className, children: [_jsx("track", { kind: "captions", src: "", label: "No captions available" }), "Your browser does not support video playback."] })); // Audio component const ArtifactAudio = ({ src, className }) => (_jsxs("audio", { src: src, controls: true, style: { width: "100%" }, className: className, children: [_jsx("track", { kind: "captions", src: "", label: "No captions available" }), "Your browser does not support audio playback."] })); // Text component const ArtifactText = ({ src, maxWidth, className }) => { const { content, loading, error } = useContentFetch(src); if (loading) return _jsx(Loader2, { className: "h-8 w-8 animate-spin" }); if (error) return _jsx(ErrorDisplay, { message: `Error loading text: ${error}` }); return (_jsx("div", { className: cn("max-h-[400px] overflow-auto p-4 bg-muted rounded-md font-mono text-sm", className), style: { maxWidth: maxWidth || "100%", }, children: _jsx("pre", { className: "m-0 whitespace-pre-wrap", children: content }) })); }; // JSON component const ArtifactJSON = ({ src, maxWidth, className }) => { const { content, loading, error } = useContentFetch(src, (response) => response.json()); if (loading) return _jsx(Loader2, { className: "h-8 w-8 animate-spin" }); if (error) return _jsx(ErrorDisplay, { message: `Error loading JSON: ${error}` }); return (_jsx("div", { className: cn("max-h-[400px] overflow-auto p-4 bg-muted rounded-md", className), style: { maxWidth: maxWidth || "100%", }, children: _jsx("pre", { className: "m-0 text-sm font-mono", children: JSON.stringify(content, null, 2) }) })); }; // HTML iframe component const ArtifactHTML = ({ src, size, maxWidth, className }) => (_jsx("iframe", { src: src, style: { width: size ? `${size}px` : "100%", height: size ? `${size}px` : "400px", maxWidth: maxWidth || "100%", }, className: cn("border border-border rounded-md", className), sandbox: "allow-scripts allow-same-origin", title: "HTML Artifact" })); // Main component export function ArtifactDisplay({ artifact, size, maxWidth = 600, showControls = true, allowZoom = true, showMetadata = false, loading = false, error, className = "", onClick, onDownload, onShare, fallback, }) { const [isZoomed, setIsZoomed] = useState(false); // Determine artifact type with memoization const artifactType = useMemo(() => { if (artifact.type) return artifact.type; if (artifact.contentType) return getArtifactTypeFromContentType(artifact.contentType); // Use bitcoin-image library to parse and validate URLs const parsed = imageProtocols.parse(artifact.src); if (parsed.isValid) { return ArtifactType.Image; } // Fallback to file extension detection for non-blockchain URLs try { const extension = artifact.src.split(".").pop()?.toLowerCase(); const extensionMap = { jpg: ArtifactType.Image, jpeg: ArtifactType.Image, png: ArtifactType.Image, gif: ArtifactType.Image, webp: ArtifactType.Image, svg: ArtifactType.SVG, mp4: ArtifactType.Video, webm: ArtifactType.Video, mov: ArtifactType.Video, mp3: ArtifactType.Audio, wav: ArtifactType.Audio, ogg: ArtifactType.Audio, html: ArtifactType.HTML, htm: ArtifactType.HTML, json: ArtifactType.JSON, txt: ArtifactType.Text, md: ArtifactType.Text, pdf: ArtifactType.PDF, }; return extensionMap[extension || ""] || ArtifactType.Unknown; } catch { return ArtifactType.Unknown; } }, [artifact.type, artifact.contentType, artifact.src]); // Handle click const handleClick = useCallback(() => { if (onClick) { onClick(artifact); } else if (allowZoom && artifactType === ArtifactType.Image) { setIsZoomed(!isZoomed); } }, [onClick, artifact, allowZoom, artifactType, isZoomed]); // Handle download with URL conversion const handleDownload = useCallback(async () => { if (onDownload) { onDownload(artifact); return; } try { const downloadUrl = await imageProtocols.getDisplayUrl(artifact.src); const link = document.createElement("a"); link.href = downloadUrl; link.download = artifact.name || "artifact"; link.click(); } catch { // Fallback to original URL const link = document.createElement("a"); link.href = artifact.src; link.download = artifact.name || "artifact"; link.click(); } }, [onDownload, artifact]); // Handle share with URL conversion const handleShare = useCallback(async () => { if (onShare) { onShare(artifact); return; } // Get display URL for sharing let shareUrl = artifact.src; try { shareUrl = await imageProtocols.getDisplayUrl(artifact.src); } catch { // Use original URL if conversion fails } if (navigator.share) { try { await navigator.share({ title: artifact.name || "Blockchain Artifact", text: artifact.description, url: shareUrl, }); } catch { navigator.clipboard?.writeText(shareUrl); } } else { navigator.clipboard?.writeText(shareUrl); } }, [onShare, artifact]); // Handle view original const handleViewOriginal = useCallback(async () => { try { const displayUrl = await imageProtocols.getDisplayUrl(artifact.src); window.open(displayUrl, "_blank"); } catch { window.open(artifact.src, "_blank"); } }, [artifact.src]); // Render content based on type const renderContent = () => { if (loading) { return (_jsxs("div", { className: "flex items-center justify-center", style: { height: size || 200 }, children: [_jsx(Loader2, { className: "h-8 w-8 animate-spin" }), _jsx("div", { style: { marginLeft: "0.5rem" }, children: _jsx("span", { className: "text-sm text-muted-foreground", children: "Loading artifact..." }) })] })); } if (error) { return _jsx(ErrorDisplay, { message: error, size: size }); } if (fallback && artifactType === ArtifactType.Unknown) { return fallback; } const contentProps = { src: artifact.src, maxWidth, className: "artifact-content", }; switch (artifactType) { case ArtifactType.Image: case ArtifactType.SVG: return (_jsx(ArtifactImage, { ...contentProps, alt: artifact.name || "Blockchain artifact", size: size, allowZoom: allowZoom, onClick: handleClick })); case ArtifactType.Video: return _jsx(ArtifactVideo, { ...contentProps }); case ArtifactType.Audio: return _jsx(ArtifactAudio, { ...contentProps }); case ArtifactType.Text: return _jsx(ArtifactText, { ...contentProps }); case ArtifactType.JSON: return _jsx(ArtifactJSON, { ...contentProps }); case ArtifactType.HTML: return _jsx(ArtifactHTML, { ...contentProps, size: size }); case ArtifactType.PDF: return (_jsxs("div", { className: "flex flex-col items-center p-4", children: [_jsx("div", { style: { marginBottom: "0.75rem" }, children: _jsx("span", { className: "text-sm text-muted-foreground", children: "PDF Preview" }) }), _jsxs(Button, { variant: "secondary", onClick: handleDownload, children: [_jsx(DownloadIcon, { width: "16", height: "16" }), "Download PDF"] })] })); default: return (_jsxs("div", { className: "flex flex-col items-center justify-center p-4", style: { height: size || 200 }, children: [_jsx(InfoCircledIcon, { className: "w-6 h-6 text-muted-foreground" }), _jsx("div", { className: "mt-2", children: _jsx("span", { className: "text-sm text-muted-foreground text-center", children: "Unknown artifact type" }) }), _jsx("div", { className: "mt-1", children: _jsx("span", { className: "text-xs text-muted-foreground", children: artifact.contentType || "No content type specified" }) })] })); } }; return (_jsxs(_Fragment, { children: [_jsx(Card, { className: className, children: _jsx(CardContent, { className: "p-4", children: _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "relative", children: renderContent() }), showControls && (_jsxs("div", { className: "flex justify-between items-center pt-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "secondary", children: artifactType.toUpperCase() }), artifact.number && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["#", artifact.number] })), artifact.size && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [(artifact.size / 1024).toFixed(1), "KB"] }))] }), _jsx(TooltipProvider, { children: _jsxs("div", { className: "flex gap-1", children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", onClick: handleViewOriginal, children: _jsx(ExternalLinkIcon, { className: "w-3.5 h-3.5" }) }) }), _jsx(TooltipContent, { children: "View original" })] }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", onClick: handleDownload, children: _jsx(DownloadIcon, { className: "w-3.5 h-3.5" }) }) }), _jsx(TooltipContent, { children: "Download" })] }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", onClick: handleShare, children: _jsx(CopyIcon, { className: "w-3.5 h-3.5" }) }) }), _jsx(TooltipContent, { children: "Share" })] })] }) })] })), showMetadata && (artifact.name || artifact.description || artifact.txid) && (_jsxs("div", { className: "flex flex-col gap-2 pt-2 border-t border-border", children: [artifact.name && (_jsx("p", { className: "text-sm font-medium", children: artifact.name })), artifact.description && (_jsx("p", { className: "text-sm text-muted-foreground", children: artifact.description })), artifact.txid && (_jsxs("p", { className: "text-xs text-muted-foreground font-mono", children: [artifact.txid.slice(0, 16), "...", artifact.txid.slice(-8), artifact.vout !== undefined && `_${artifact.vout}`] }))] }))] }) }) }), _jsxs(Dialog, { open: isZoomed && artifactType === ArtifactType.Image, onOpenChange: setIsZoomed, children: [_jsx(DialogOverlay, {}), _jsx(DialogContent, { className: "max-w-[95vw] w-full h-[90vh] flex items-center justify-center p-0 bg-transparent border-0", children: _jsx(BitcoinImage, { src: artifact.src, alt: artifact.name || "Zoomed artifact", style: { maxWidth: "90vw", maxHeight: "90vh", objectFit: "contain", }, fallback: _jsx("img", { src: artifact.src, alt: artifact.name || "Zoomed artifact", style: { maxWidth: "90vw", maxHeight: "90vh", objectFit: "contain", } }) }) })] })] })); }