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