Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
1import { Dialog, DialogPanel } from "@headlessui/react";
2import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/solid";
3import { memo, useEffect, useMemo, useState } from "react";
4import { useHotkeys } from "react-hotkeys-hook";
5import { Spinner } from "@/components/Shared/UI";
6import cn from "@/helpers/cn";
7
8interface LightBoxProps {
9 show: boolean;
10 onClose: () => void;
11 images: string[];
12 initialIndex?: number;
13}
14
15const LightBox = ({
16 show,
17 onClose,
18 images,
19 initialIndex = 0
20}: LightBoxProps) => {
21 const [currentIndex, setCurrentIndex] = useState(initialIndex);
22 const [isLoading, setIsLoading] = useState(true);
23
24 const currentImage = useMemo(
25 () => images[currentIndex],
26 [images, currentIndex]
27 );
28
29 useEffect(() => {
30 if (show) {
31 setCurrentIndex(initialIndex);
32 setIsLoading(true);
33 }
34 }, [show, initialIndex]);
35
36 const handleNext = () => {
37 setCurrentIndex((prev) => {
38 const next = Math.min(prev + 1, images.length - 1);
39 if (next !== prev) setIsLoading(true);
40 return next;
41 });
42 };
43
44 const handlePrevious = () => {
45 setCurrentIndex((prev) => {
46 const prevIndex = Math.max(prev - 1, 0);
47 if (prevIndex !== prev) setIsLoading(true);
48 return prevIndex;
49 });
50 };
51
52 useHotkeys("escape", onClose, { enabled: show });
53 useHotkeys("arrowright", handleNext, { enabled: show });
54 useHotkeys("arrowleft", handlePrevious, { enabled: show });
55
56 return (
57 <Dialog className="relative z-50" onClose={onClose} open={show}>
58 <div
59 aria-hidden="true"
60 className="fixed inset-0 bg-gray-500/75 backdrop-blur-sm dark:bg-gray-900/80"
61 />
62 <div className="fixed inset-0 flex items-center justify-center">
63 <DialogPanel>
64 {isLoading && (
65 <div className="absolute inset-0 flex items-center justify-center">
66 <Spinner className="text-white" size="md" />
67 </div>
68 )}
69 {images.length > 1 && (
70 <>
71 <button
72 className={cn(
73 "fixed top-1/2 left-4 rounded-full bg-black/50 p-2 text-white md:left-6 md:p-3",
74 { "cursor-not-allowed opacity-50": currentIndex === 0 }
75 )}
76 disabled={currentIndex === 0}
77 onClick={handlePrevious}
78 type="button"
79 >
80 <ArrowLeftIcon className="size-6" />
81 </button>
82 <button
83 className={cn(
84 "fixed top-1/2 right-4 rounded-full bg-black/50 p-2 text-white md:right-6 md:p-3",
85 {
86 "cursor-not-allowed opacity-50":
87 currentIndex === images.length - 1
88 }
89 )}
90 disabled={currentIndex === images.length - 1}
91 onClick={handleNext}
92 type="button"
93 >
94 <ArrowRightIcon className="size-6" />
95 </button>
96 </>
97 )}
98 <img
99 alt={`${currentIndex + 1} of ${images.length}`}
100 className="max-h-[90vh] w-auto max-w-full cursor-zoom-in touch-manipulation select-none object-contain"
101 draggable={false}
102 loading="lazy"
103 onClick={() => window.open(currentImage, "_blank", "noopener")}
104 onError={() => setIsLoading(false)}
105 onLoad={() => setIsLoading(false)}
106 src={currentImage}
107 />
108 </DialogPanel>
109 </div>
110 </Dialog>
111 );
112};
113
114export default memo(LightBox);