119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo } from 'react';
|
|
import { useQueries } from '@tanstack/react-query';
|
|
import LightGallery from 'lightgallery/react';
|
|
import lgThumbnail from 'lightgallery/plugins/thumbnail';
|
|
import lgZoom from 'lightgallery/plugins/zoom';
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { protectedMediaService } from '@/services/api';
|
|
|
|
interface RepairProofGalleryProps {
|
|
imageUrls: string[];
|
|
maxVisibleImages?: number;
|
|
}
|
|
|
|
const MAX_VISIBLE_IMAGES = 8;
|
|
|
|
export function RepairProofGallery({
|
|
imageUrls,
|
|
maxVisibleImages = MAX_VISIBLE_IMAGES,
|
|
}: RepairProofGalleryProps) {
|
|
const queries = useQueries({
|
|
queries: imageUrls.map((url) => ({
|
|
queryKey: ['protected-media', 'v2', url],
|
|
queryFn: () => protectedMediaService.getBlob(url),
|
|
staleTime: Infinity,
|
|
gcTime: 1000 * 60 * 30,
|
|
})),
|
|
});
|
|
|
|
const blobs = queries.map((query) => query.data);
|
|
const objectUrls = useMemo(
|
|
() => blobs.map((blob) => (blob ? URL.createObjectURL(blob) : null)),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[...blobs],
|
|
);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
objectUrls.forEach((url) => {
|
|
if (url) URL.revokeObjectURL(url);
|
|
});
|
|
},
|
|
[objectUrls],
|
|
);
|
|
|
|
if (queries.some((query) => query.isLoading)) {
|
|
return (
|
|
<div className="grid grid-cols-4 gap-1 overflow-hidden rounded-lg">
|
|
{imageUrls.slice(0, maxVisibleImages).map((url, index) => (
|
|
<Skeleton
|
|
key={`${url}-${index}`}
|
|
className="aspect-square rounded-lg"
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const images = objectUrls.flatMap((url, index) =>
|
|
url ? [{ url, originalIndex: index }] : [],
|
|
);
|
|
const visibleCount = Math.min(maxVisibleImages, images.length);
|
|
const overflowCount = images.length - visibleCount;
|
|
|
|
if (images.length === 0) {
|
|
return (
|
|
<div className="rounded-lg border border-dashed py-8 text-center text-sm text-muted-foreground">
|
|
Repair images are unavailable.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<LightGallery
|
|
elementClassNames="grid grid-cols-4 gap-1 overflow-hidden rounded-lg"
|
|
mode="lg-fade"
|
|
speed={400}
|
|
plugins={[lgThumbnail, lgZoom]}
|
|
thumbnail
|
|
download={false}
|
|
mobileSettings={{ controls: true, showCloseIcon: true, download: false }}
|
|
>
|
|
{images.map(({ url, originalIndex }, index) => {
|
|
const isVisible = index < visibleCount;
|
|
const isOverflowTile = overflowCount > 0 && index === visibleCount - 1;
|
|
|
|
return (
|
|
<a
|
|
key={imageUrls[originalIndex]}
|
|
data-src={url}
|
|
data-sub-html={`<p>Repair evidence ${originalIndex + 1}</p>`}
|
|
className={isVisible ? 'group relative aspect-square' : 'hidden'}
|
|
aria-label={
|
|
isOverflowTile
|
|
? `View all ${images.length} repair images`
|
|
: `View repair image ${originalIndex + 1} of ${images.length}`
|
|
}
|
|
>
|
|
{/* Blob URLs cannot be handled by next/image. */}
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={url}
|
|
alt={`Repair evidence ${originalIndex + 1}`}
|
|
className="size-full object-cover transition duration-300 group-hover:scale-[1.03] group-hover:brightness-90"
|
|
/>
|
|
{isOverflowTile ? (
|
|
<span className="absolute inset-0 flex items-center justify-center bg-black/55 px-2 text-center text-sm font-semibold text-white transition-colors group-hover:bg-black/65 sm:text-base">
|
|
+{overflowCount} more
|
|
</span>
|
|
) : null}
|
|
</a>
|
|
);
|
|
})}
|
|
</LightGallery>
|
|
);
|
|
}
|