Fork of atp.tools as a universal profile for people on the ATmosphere
at main 111 lines 3.2 kB view raw
1import { X } from "lucide-react"; 2import { useState } from "preact/hooks"; 3import { getBlueskyCdnLink } from "./appBskyEmbedImages"; 4 5export default function BlobLayout({ 6 did, 7 dollar_link: ref, 8 mimeType, 9 author_pds: pds, 10}: { 11 did: string; 12 dollar_link?: string; 13 mimeType?: string; 14 author_pds?: string; 15}) { 16 if (mimeType === undefined || ref === undefined) 17 return <>Unsupported blob type</>; 18 if (mimeType?.includes("image")) { 19 return ImageGridLayout({ 20 images: [ 21 { 22 url: getBlueskyCdnLink(did, ref, "jpeg"), 23 }, 24 ], 25 }); 26 } 27 if (pds == "") return `blob from ${did} with cid ${ref}`; 28 return ( 29 <a 30 className="text-blue-700 dark:text-blue-400" 31 href={`${pds}xrpc/com.atproto.sync.getBlob?did=${did}&cid=${ref}`} 32 > 33 Download {mimeType} file at{" "} 34 {pds?.replace("https://", "").replace("/", "")} ({ref}) 35 </a> 36 ); 37} 38 39interface ImageInfo { 40 url: string; 41 alt?: string; 42} 43 44export const ImageGridLayout = ({ images }: { images: ImageInfo[] }) => { 45 const [selectedImage, setSelectedImage] = useState<number | null>(null); 46 const imageCount = images.length; 47 48 // Different grid layouts based on number of images 49 const gridClassName = 50 { 51 1: "grid-cols-2", 52 2: "grid-cols-2", 53 3: "grid-cols-2", 54 4: "grid-cols-2", 55 }[Math.min(imageCount, 4)] || "grid-cols-2"; 56 57 return ( 58 <> 59 <div className={`grid ${gridClassName} gap-2 w-full`}> 60 {images.map((image, i) => ( 61 <div 62 key={i} 63 className={`relative overflow-hidden rounded-lg cursor-pointer ${ 64 imageCount === 3 && i === 0 ? "col-span-2" : "" 65 }`} 66 onClick={() => setSelectedImage(i)} 67 > 68 <img 69 src={image.url} 70 alt={image.alt || ""} 71 className={`w-full h-full cursor-pointer object-cover transition-transform duration-300 hover:scale-[101%] ${imageCount > 1 && "max-h-64"}`} 72 style={{ 73 aspectRatio: imageCount === 1 ? "" : "1/1", 74 }} 75 loading="lazy" 76 /> 77 </div> 78 ))} 79 </div> 80 81 {selectedImage !== null && ( 82 <> 83 {/* Image Preview */} 84 <div 85 className="fixed inset-0 bg-black/80 flex flex-col gap-2 items-center justify-center z-50" 86 onClick={() => setSelectedImage(null)} 87 > 88 <img 89 src={images[selectedImage].url} 90 alt={images[selectedImage].alt || ""} 91 className="max-h-[90vh] max-w-[90vw] object-contain" 92 /> 93 {images[selectedImage].alt && ( 94 <div className="text-white"> 95 Alt text: {images[selectedImage].alt} 96 </div> 97 )} 98 </div> 99 <div className="fixed top-2 right-2 z-50"> 100 <button 101 className="text-blue-100 hover:text-red-400 transition-colors duration-300" 102 onClick={() => setSelectedImage(null)} 103 > 104 <X /> 105 </button> 106 </div> 107 </> 108 )} 109 </> 110 ); 111};