bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
311 lines (310 loc) • 17.5 kB
JavaScript
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",
} }) }) })] })] }));
}