Fork of atp.tools as a universal profile for people on the ATmosphere
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};