返回 presentation-ai
UserImagesGallery.tsx
1 "use client";
2
3 import {
4 getUserImages,
5 type Image as GeneratedImage,
6 } from "@/app/_actions/apps/image-studio/fetch";
7 import { Button } from "@/components/ui/button";
8 import { Label } from "@/components/ui/label";
9 import { ScrollArea } from "@/components/ui/scroll-area";
10 import { Skeleton } from "@/components/ui/skeleton";
11 import { cn } from "@/lib/utils";
12 import { useInfiniteQuery } from "@tanstack/react-query";
13 import { Check } from "lucide-react";
14 import { useEffect } from "react";
15 import { useInView } from "react-intersection-observer";
16
17 interface UserImagesGalleryProps {
18 onImageSelect: (url: string, prompt: string) => void;
19 justGeneratedImages?: GeneratedImage[];
20 onClearRecent?: () => void;
21 className?: string;
22 }
23
24 export function UserImagesGallery({
25 onImageSelect,
26 justGeneratedImages = [],
27 onClearRecent,
28 className,
29 }: UserImagesGalleryProps) {
30 const { ref, inView } = useInView();
31
32 const {
33 data: userImagesData,
34 fetchNextPage,
35 hasNextPage,
36 isFetchingNextPage,
37 isLoading,
38 } = useInfiniteQuery({
39 queryKey: ["user-generated-images"],
40 queryFn: async ({ pageParam = 1 }) => {
41 const images = await getUserImages({ page: pageParam, limit: 20 });
42 return images;
43 },
44 initialPageParam: 1,
45 getNextPageParam: (lastPage, allPages) => {
46 return lastPage.length === 20 ? allPages.length + 1 : undefined;
47 },
48 });
49
50 useEffect(() => {
51 if (inView && hasNextPage) {
52 void fetchNextPage();
53 }
54 }, [inView, fetchNextPage, hasNextPage]);
55
56 return (
57 <div className={cn("flex flex-col gap-6", className)}>
58 {justGeneratedImages.length > 0 && (
59 <div className="space-y-3">
60 <div className="flex items-center justify-between">
61 <Label className="text-sm font-medium">Just generated</Label>
62 {onClearRecent && (
63 <Button
64 variant="ghost"
65 size="sm"
66 className="h-auto p-0 text-xs"
67 onClick={onClearRecent}
68 >
69 Show history
70 </Button>
71 )}
72 </div>
73 <div className="grid grid-cols-2 gap-2">
74 {justGeneratedImages.map((img) => (
75 <figure
76 key={img.id}
77 className="relative aspect-square overflow-hidden rounded-lg border-2 border-primary shadow-md"
78 >
79 {/** biome-ignore lint/performance/noImgElement: Necessary for user provided links */}
80 <img
81 src={img.url}
82 alt={img.prompt}
83 className="h-full w-full object-cover"
84 />
85 <figcaption className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 transition-opacity hover:opacity-100">
86 <Button
87 size="icon"
88 variant="secondary"
89 className="h-8 w-8 rounded-full"
90 onClick={() => onImageSelect(img.url, img.prompt)}
91 >
92 <Check className="h-4 w-4" />
93 </Button>
94 </figcaption>
95 </figure>
96 ))}
97 </div>
98 </div>
99 )}
100
101 <div className="space-y-3">
102 <Label className="text-sm font-medium">Your AI images</Label>
103 <ScrollArea className="h-[420px] rounded-lg border bg-muted/30 p-3">
104 <div className="grid grid-cols-2 gap-2 pb-4">
105 {userImagesData?.pages.map((page) =>
106 page.map((img) => (
107 <button
108 key={img.id}
109 type="button"
110 onClick={() => onImageSelect(img.url, img.prompt)}
111 className="group relative aspect-square overflow-hidden rounded-lg border border-border bg-background text-left"
112 >
113 {/** biome-ignore lint/performance/noImgElement: Necessary for user provided links */}
114 <img
115 src={img.url}
116 alt={img.prompt}
117 loading="lazy"
118 className="h-full w-full object-cover transition-transform group-hover:scale-105"
119 />
120 <span className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-1 bg-black/40 text-[11px] font-medium tracking-wide text-white uppercase opacity-0 transition-opacity group-hover:opacity-100">
121 <Check className="h-4 w-4" />
122 Use image
123 </span>
124 </button>
125 )),
126 )}
127
128 {(isLoading || isFetchingNextPage) &&
129 Array.from({ length: 4 }).map((_, index) => (
130 <Skeleton key={index} className="aspect-square rounded-lg" />
131 ))}
132
133 <div ref={ref} className="col-span-2 h-4" />
134 </div>
135
136 {!isLoading &&
137 !isFetchingNextPage &&
138 (!userImagesData ||
139 userImagesData.pages.every((page) => page.length === 0)) && (
140 <div className="flex h-32 items-center justify-center text-sm text-muted-foreground">
141 No generated images yet.
142 </div>
143 )}
144 </ScrollArea>
145 </div>
146 </div>
147 );
148 }
149
149 lines Plain Text