Rework #6
					 4 changed files with 88 additions and 15 deletions
				
			
		| 
						 | 
				
			
			@ -17,6 +17,7 @@
 | 
			
		|||
    "@radix-ui/react-separator": "^1.0.3",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.0.2",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^1.0.7",
 | 
			
		||||
    "@tanstack/react-virtual": "^3.2.1",
 | 
			
		||||
    "class-variance-authority": "^0.7.0",
 | 
			
		||||
    "clsx": "^2.1.0",
 | 
			
		||||
    "dbtype": "workspace:*",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,6 +30,7 @@ export function useSearchGalleryInfinite(searchParams: SearchParams = {}) {
 | 
			
		|||
    {
 | 
			
		||||
        data: Document[];
 | 
			
		||||
        nextCursor: number | null;
 | 
			
		||||
        startCursor: number | null;
 | 
			
		||||
        hasMore: boolean;
 | 
			
		||||
    }
 | 
			
		||||
    >((index, previous) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +49,7 @@ export function useSearchGalleryInfinite(searchParams: SearchParams = {}) {
 | 
			
		|||
        const res = await fetcher(url);
 | 
			
		||||
        return {
 | 
			
		||||
            data: res,
 | 
			
		||||
            startCursor: res.length === 0 ? null : res[0].id,
 | 
			
		||||
            nextCursor: res.length === 0 ? null : res[res.length - 1].id,
 | 
			
		||||
            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 { Spinner } from "../components/Spinner.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() {
 | 
			
		||||
    const search = useSearch();
 | 
			
		||||
| 
						 | 
				
			
			@ -19,21 +21,50 @@ export default function Gallery() {
 | 
			
		|||
        limit: limit ? Number.parseInt(limit) : 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) {
 | 
			
		||||
        return <div className="p-4">Loading...</div>
 | 
			
		||||
    }
 | 
			
		||||
    if (error) {
 | 
			
		||||
        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 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 />
 | 
			
		||||
        {(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>}
 | 
			
		||||
                {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>
 | 
			
		||||
| 
						 | 
				
			
			@ -41,21 +72,42 @@ export default function Gallery() {
 | 
			
		|||
        {
 | 
			
		||||
            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
 | 
			
		||||
            data?.map((docs) => {
 | 
			
		||||
                return docs?.data?.map((x) => {
 | 
			
		||||
                    return (
 | 
			
		||||
                        <GalleryCard doc={x} key={x.id} />
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
            virtualItems.map((item) => {
 | 
			
		||||
                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 (
 | 
			
		||||
                            <GalleryCard doc={x} key={x.id} />
 | 
			
		||||
                        );
 | 
			
		||||
                    })}
 | 
			
		||||
                </div>
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        {
 | 
			
		||||
            <Button className="w-full" onClick={() => setSize(size + 1)}
 | 
			
		||||
                disabled={isReachingEnd || isLoadingMore}
 | 
			
		||||
            > {isLoadingMore && <Spinner className="mr-1" />}{size + 1} Load more</Button>
 | 
			
		||||
        }
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +122,7 @@ function Search() {
 | 
			
		|||
        <TagInput className="flex-1" input={word} onInputChange={setWord}
 | 
			
		||||
            tags={tags} onTagsChange={setTags}
 | 
			
		||||
        />
 | 
			
		||||
        <Button className="flex-none" onClick={()=>{
 | 
			
		||||
        <Button className="flex-none" onClick={() => {
 | 
			
		||||
            const params = new URLSearchParams();
 | 
			
		||||
            if (tags.length > 0) {
 | 
			
		||||
                for (const tag of tags) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -32,6 +32,9 @@ importers:
 | 
			
		|||
      '@radix-ui/react-tooltip':
 | 
			
		||||
        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)
 | 
			
		||||
      '@tanstack/react-virtual':
 | 
			
		||||
        specifier: ^3.2.1
 | 
			
		||||
        version: 3.2.1(react-dom@18.2.0)(react@18.2.0)
 | 
			
		||||
      class-variance-authority:
 | 
			
		||||
        specifier: ^0.7.0
 | 
			
		||||
        version: 0.7.0
 | 
			
		||||
| 
						 | 
				
			
			@ -1786,6 +1789,21 @@ packages:
 | 
			
		|||
      defer-to-connect: 2.0.1
 | 
			
		||||
    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:
 | 
			
		||||
    resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
 | 
			
		||||
    dev: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue