返回 presentation-ai
UploadedImagesGrid.tsx
1 "use client";
2
3 import { useInfiniteQuery } from "@tanstack/react-query";
4 import { Images } from "lucide-react";
5 import Image from "next/image";
6 import { useEffect } from "react";
7 import { useInView } from "react-intersection-observer";
8
9 import { getUploadedImages } from "@/app/_actions/presentation/uploaded-image-actions";
10 import { ScrollArea } from "@/components/ui/scroll-area";
11 import { Skeleton } from "@/components/ui/skeleton";
12
13 const UPLOADED_IMAGES_PAGE_SIZE = 30;
14
15 type UploadedImage = Awaited<ReturnType<typeof getUploadedImages>>[number];
16
17 interface UploadedImagesGridProps {
18 onImageSelect: (image: UploadedImage) => void;
19 }
20
21 export function UploadedImagesGrid({ onImageSelect }: UploadedImagesGridProps) {
22 const { ref, inView } = useInView();
23
24 const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
25 useInfiniteQuery({
26 queryKey: ["presentation-uploaded-images"],
27 queryFn: ({ pageParam }) =>
28 getUploadedImages({
29 page: pageParam,
30 limit: UPLOADED_IMAGES_PAGE_SIZE,
31 }),
32 initialPageParam: 1,
33 getNextPageParam: (lastPage, allPages) =>
34 lastPage.length === UPLOADED_IMAGES_PAGE_SIZE
35 ? allPages.length + 1
36 : undefined,
37 });
38
39 useEffect(() => {
40 if (inView && hasNextPage && !isFetchingNextPage) {
41 void fetchNextPage();
42 }
43 }, [fetchNextPage, hasNextPage, inView, isFetchingNextPage]);
44
45 const uploadedImages = data?.pages.flat() ?? [];
46 const showEmptyState = !isLoading && uploadedImages.length === 0;
47
48 return (
49 <ScrollArea className="-mx-2 flex-1 px-2">
50 <div className="grid grid-cols-3 gap-2 pb-4">
51 {uploadedImages.map((image) => (
52 <button
53 key={image.id}
54 type="button"
55 onClick={() => onImageSelect(image)}
56 className="group relative aspect-square overflow-hidden rounded-lg border border-border bg-muted/30 text-left outline-none transition hover:border-primary/60 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/30"
57 title={image.name}
58 >
59 <Image
60 unoptimized
61 width={400}
62 height={300}
63 src={image.url}
64 alt={image.name}
65 className="h-full w-full object-cover transition-transform group-hover:scale-105"
66 loading="lazy"
67 />
68 <span className="absolute inset-x-0 bottom-0 line-clamp-2 bg-black/60 px-2 py-1 text-[11px] leading-tight text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
69 {image.name}
70 </span>
71 </button>
72 ))}
73
74 {(isLoading || isFetchingNextPage) &&
75 Array.from({ length: 6 }, (_, index) => (
76 <Skeleton key={index} className="aspect-square rounded-lg" />
77 ))}
78
79 <div ref={ref} className="col-span-3 h-4" />
80
81 {showEmptyState && (
82 <div className="col-span-3 flex flex-col items-center justify-center gap-3 py-12 text-center text-muted-foreground">
83 <Images className="size-8" />
84 <p className="text-sm">No uploaded images found.</p>
85 </div>
86 )}
87 </div>
88 </ScrollArea>
89 );
90 }
91
91 lines Plain Text