logipedia/src/components/category/grid/infinite-category-grid.tsx

69 lines
2.1 KiB
TypeScript

"use client";
import { api } from "@/trpc/react";
import React from "react";
import CategoryCard from "../category-card";
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
import { Skeleton } from "@/components/ui/skeleton";
import { CATEGORY_GRID_CLASS } from "./category-grid";
import CategoryFilterBar, { CategoryFilter } from "../category-filter-bar";
export default function InfiniteCategoryGrid() {
const [filter, setFilter] = React.useState<CategoryFilter>({});
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
api.category.getByCursor.useInfiniteQuery(
{
filter,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 60 * 4 * 1000, // 4 minutes stale time
refetchOnMount: false, // Prevents unnecessary refetching
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
},
);
// Calculate all visible items across all loaded pages
const allItems = React.useMemo(() => {
return data?.pages.flatMap((page) => page.items) || [];
}, [data]);
// Ref for bottom observation
const bottomObserverRef = React.useRef(null);
useInfiniteItemsObserver({
bottomObserverRef,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
});
return (
<div className="relative space-y-4">
<CategoryFilterBar onFilterUpdate={setFilter} />
<menu className={`${CATEGORY_GRID_CLASS} overflow-auto`}>
{data?.pages?.length
? allItems.map((category, idx) => (
<li key={`category-${idx}`}>
<CategoryCard {...category} />
</li>
))
: null}
{/* Loading indicator */}
{(isLoading || isFetchingNextPage) &&
Array.from(new Array(isLoading ? 16 : 4).keys()).map((idx) => (
<li key={idx}>
<Skeleton className="size-full min-h-20" />
</li>
))}
{/* Bottom observer element */}
{hasNextPage && (
<li ref={bottomObserverRef} className="col-span-full h-12" />
)}
</menu>
</div>
);
}