Rework #6
@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@tanstack/react-virtual": "^3.2.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"dbtype": "workspace:*",
|
"dbtype": "workspace:*",
|
||||||
|
@ -30,6 +30,7 @@ export function useSearchGalleryInfinite(searchParams: SearchParams = {}) {
|
|||||||
{
|
{
|
||||||
data: Document[];
|
data: Document[];
|
||||||
nextCursor: number | null;
|
nextCursor: number | null;
|
||||||
|
startCursor: number | null;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
}
|
}
|
||||||
>((index, previous) => {
|
>((index, previous) => {
|
||||||
@ -48,6 +49,7 @@ export function useSearchGalleryInfinite(searchParams: SearchParams = {}) {
|
|||||||
const res = await fetcher(url);
|
const res = await fetcher(url);
|
||||||
return {
|
return {
|
||||||
data: res,
|
data: res,
|
||||||
|
startCursor: res.length === 0 ? null : res[0].id,
|
||||||
nextCursor: res.length === 0 ? null : res[res.length - 1].id,
|
nextCursor: res.length === 0 ? null : res[res.length - 1].id,
|
||||||
hasMore: limit ? res.length === limit : (res.length === 20),
|
hasMore: limit ? res.length === limit : (res.length === 20),
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,9 @@ import TagBadge from "@/components/gallery/TagBadge.tsx";
|
|||||||
import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
|
import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
|
||||||
import { Spinner } from "../components/Spinner.tsx";
|
import { Spinner } from "../components/Spinner.tsx";
|
||||||
import TagInput from "@/components/gallery/TagInput.tsx";
|
import TagInput from "@/components/gallery/TagInput.tsx";
|
||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Separator } from "@/components/ui/separator.tsx";
|
||||||
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
|
||||||
export default function Gallery() {
|
export default function Gallery() {
|
||||||
const search = useSearch();
|
const search = useSearch();
|
||||||
@ -19,6 +21,31 @@ export default function Gallery() {
|
|||||||
limit: limit ? Number.parseInt(limit) : undefined,
|
limit: limit ? Number.parseInt(limit) : undefined,
|
||||||
cursor: cursor ? Number.parseInt(cursor) : undefined
|
cursor: cursor ? Number.parseInt(cursor) : undefined
|
||||||
});
|
});
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: size,
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||||
|
getScrollElement: () => parentRef.current!,
|
||||||
|
estimateSize: (index) => {
|
||||||
|
const docs = data?.[index];
|
||||||
|
if (!docs) return 8;
|
||||||
|
return docs.data.length * (200 + 8) + 37 + 8;
|
||||||
|
},
|
||||||
|
overscan: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const virtualItems = virtualizer.getVirtualItems();
|
||||||
|
useEffect(() => {
|
||||||
|
const lastItems = virtualItems.slice(-1);
|
||||||
|
// console.log(virtualItems);
|
||||||
|
if (lastItems.some(x => x.index >= size - 1)) {
|
||||||
|
const last = lastItems[0];
|
||||||
|
const docs = data?.[last.index];
|
||||||
|
if (docs?.hasMore) {
|
||||||
|
setSize(size + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [virtualItems, setSize, size, data]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="p-4">Loading...</div>
|
return <div className="p-4">Loading...</div>
|
||||||
@ -26,14 +53,18 @@ export default function Gallery() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return <div className="p-4">Error: {String(error)}</div>
|
return <div className="p-4">Error: {String(error)}</div>
|
||||||
}
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div className="p-4">No data</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
|
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
|
||||||
const isReachingEnd = data && data[size - 1]?.hasMore === false;
|
const isReachingEnd = data && data[size - 1]?.hasMore === false;
|
||||||
|
|
||||||
return (<div className="p-4 grid gap-2 overflow-auto h-screen items-start content-start">
|
return (<div className="p-4 grid gap-2 overflow-auto h-dvh items-start content-start" ref={parentRef}>
|
||||||
<Search />
|
<Search />
|
||||||
{(word || tags) &&
|
{(word || tags) &&
|
||||||
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md">
|
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md z-20">
|
||||||
{word && <span className="text-primary-foreground ml-4">Search: {word}</span>}
|
{word && <span className="text-primary-foreground ml-4">Search: {word}</span>}
|
||||||
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex">{tags.split(",").map(x => <TagBadge tagname={x} key={x} />)}</ul></span>}
|
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex">{tags.split(",").map(x => <TagBadge tagname={x} key={x} />)}</ul></span>}
|
||||||
</div>
|
</div>
|
||||||
@ -41,21 +72,42 @@ export default function Gallery() {
|
|||||||
{
|
{
|
||||||
data?.length === 0 && <div className="p-4 text-3xl">No results</div>
|
data?.length === 0 && <div className="p-4 text-3xl">No results</div>
|
||||||
}
|
}
|
||||||
|
<div className="w-full relative"
|
||||||
|
style={{ height: virtualizer.getTotalSize() }}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
// TODO: date based grouping
|
// TODO: date based grouping
|
||||||
data?.map((docs) => {
|
virtualItems.map((item) => {
|
||||||
return docs?.data?.map((x) => {
|
const isLoaderRow = item.index === size - 1 && isLoadingMore;
|
||||||
|
if (isLoaderRow) {
|
||||||
|
return <div key={item.index} className="w-full flex justify-center top-0 left-0 absolute"
|
||||||
|
style={{
|
||||||
|
height: `${item.size}px`,
|
||||||
|
transform: `translateY(${item.start}px)`
|
||||||
|
}}>
|
||||||
|
<Spinner />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
const docs = data[item.index];
|
||||||
|
if (!docs) return null;
|
||||||
|
return <div className="w-full grid gap-2 content-start top-0 left-0 absolute mb-2" key={item.index}
|
||||||
|
style={{
|
||||||
|
height: `${item.size}px`,
|
||||||
|
transform: `translateY(${item.start}px)`
|
||||||
|
}}>
|
||||||
|
{docs.startCursor && <div>
|
||||||
|
<h3 className="text-3xl">Start with {docs.startCursor}</h3>
|
||||||
|
<Separator/>
|
||||||
|
</div>}
|
||||||
|
{docs?.data?.map((x) => {
|
||||||
return (
|
return (
|
||||||
<GalleryCard doc={x} key={x.id} />
|
<GalleryCard doc={x} key={x.id} />
|
||||||
);
|
);
|
||||||
});
|
})}
|
||||||
|
</div>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
{
|
</div>
|
||||||
<Button className="w-full" onClick={() => setSize(size + 1)}
|
|
||||||
disabled={isReachingEnd || isLoadingMore}
|
|
||||||
> {isLoadingMore && <Spinner className="mr-1" />}{size + 1} Load more</Button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@ importers:
|
|||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.7(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@tanstack/react-virtual':
|
||||||
|
specifier: ^3.2.1
|
||||||
|
version: 3.2.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
@ -1786,6 +1789,21 @@ packages:
|
|||||||
defer-to-connect: 2.0.1
|
defer-to-connect: 2.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@tanstack/react-virtual@3.2.1(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-i9Nt0ssIh2bSjomJZlr6Iq5usT/9+ewo2/fKHRNk6kjVKS8jrhXbnO8NEawarCuBx/efv0xpoUUKKGxa0cQb4Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/virtual-core': 3.2.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/virtual-core@3.2.1:
|
||||||
|
resolution: {integrity: sha512-nO0d4vRzsmpBQCJYyClNHPPoUMI4nXNfrm6IcCRL33ncWMoNVpURh9YebEHPw8KrtsP2VSJIHE4gf4XFGk1OGg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tokenizer/token@0.3.0:
|
/@tokenizer/token@0.3.0:
|
||||||
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
Loading…
Reference in New Issue
Block a user