lazy loading

This commit is contained in:
Josh Hawkins 2025-10-27 11:57:53 -05:00
parent 9924b89d2b
commit 074af52b81

View File

@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useSWR from "swr"; import useSWR from "swr";
import { import {
@ -15,11 +15,13 @@ import { cn } from "@/lib/utils";
import { Event } from "@/types/event"; import { Event } from "@/types/event";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import { isDesktop, isMobile } from "react-device-detect"; import { isDesktop, isMobile } from "react-device-detect";
import ActivityIndicator from "../indicators/activity-indicator";
type ImagePickerProps = { type ImagePickerProps = {
selectedImageId?: string; selectedImageId?: string;
setSelectedImageId?: (id: string) => void; setSelectedImageId?: (id: string) => void;
camera: string; camera: string;
limit?: number;
direct?: boolean; direct?: boolean;
className?: string; className?: string;
}; };
@ -28,6 +30,7 @@ export default function ImagePicker({
selectedImageId, selectedImageId,
setSelectedImageId, setSelectedImageId,
camera, camera,
limit = 100,
direct = false, direct = false,
className, className,
}: ImagePickerProps) { }: ImagePickerProps) {
@ -35,9 +38,10 @@ export default function ImagePicker({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [loadedImages, setLoadedImages] = useState<Set<string>>(new Set());
const { data: events } = useSWR<Event[]>( const { data: events } = useSWR<Event[]>(
`events?camera=${camera}&limit=100`, `events?camera=${camera}&limit=${limit}`,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
}, },
@ -74,6 +78,10 @@ export default function ImagePicker({
[setSelectedImageId, direct], [setSelectedImageId, direct],
); );
const handleImageLoad = useCallback((imageId: string) => {
setLoadedImages((prev) => new Set(prev).add(imageId));
}, []);
const renderSearchInput = () => ( const renderSearchInput = () => (
<Input <Input
type="text" type="text"
@ -95,7 +103,7 @@ export default function ImagePicker({
<div <div
key={image.id} key={image.id}
className={cn( className={cn(
"aspect-square cursor-pointer overflow-hidden rounded-lg border-2 bg-background transition-all", "relative aspect-square cursor-pointer overflow-hidden rounded-lg border-2 bg-background transition-all",
selectedImageId === image.id && selectedImageId === image.id &&
"border-selected ring-2 ring-selected", "border-selected ring-2 ring-selected",
)} )}
@ -105,13 +113,25 @@ export default function ImagePicker({
alt={image.label} alt={image.label}
className="h-full w-full object-cover" className="h-full w-full object-cover"
onClick={() => handleImageSelect(image.id)} onClick={() => handleImageSelect(image.id)}
onLoad={() => handleImageLoad(image.id)}
loading="lazy"
/> />
{!loadedImages.has(image.id) && (
<div className="absolute inset-0 flex items-center justify-center">
<ActivityIndicator />
</div>
)}
</div> </div>
)) ))
)} )}
</div> </div>
); );
// Reset loaded images when images change
useEffect(() => {
setLoadedImages(new Set());
}, [images]);
if (direct) { if (direct) {
return ( return (
<div ref={containerRef} className={className}> <div ref={containerRef} className={className}>
@ -141,15 +161,24 @@ export default function ImagePicker({
<div className="hover:cursor-pointer"> <div className="hover:cursor-pointer">
<div className="my-3 flex w-full flex-row items-center justify-between gap-2"> <div className="my-3 flex w-full flex-row items-center justify-between gap-2">
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<img <div className="relative h-8 w-8">
src={ <img
selectedImage src={
? `${apiHost}api/events/${selectedImage.id}/thumbnail.webp` selectedImage
: `${apiHost}clips/triggers/${camera}/${selectedImageId}.webp` ? `${apiHost}api/events/${selectedImage.id}/thumbnail.webp`
} : `${apiHost}clips/triggers/${camera}/${selectedImageId}.webp`
alt={selectedImage?.label || "Selected image"} }
className="h-8 w-8 rounded object-cover" alt={selectedImage?.label || "Selected image"}
/> className="h-8 w-8 rounded object-cover"
onLoad={() => handleImageLoad(selectedImageId || "")}
loading="lazy"
/>
{selectedImageId && !loadedImages.has(selectedImageId) && (
<div className="absolute inset-0 flex items-center justify-center">
<ActivityIndicator />
</div>
)}
</div>
<div className="text-sm smart-capitalize"> <div className="text-sm smart-capitalize">
{selectedImage?.label || selectedImageId} {selectedImage?.label || selectedImageId}
{selectedImage?.sub_label {selectedImage?.sub_label