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

69 lines
2.0 KiB
TypeScript

"use client";
import { api } from "@/trpc/react";
import React from "react";
import { ARTICLE_GRID_CLASS } from "./article-grid";
import ArticleCard from "../article-card";
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
import { Skeleton } from "@/components/ui/skeleton";
import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar";
function InfiniteArticlesGrid() {
const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
undefined,
);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
api.article.getByCursor.useInfiniteQuery(
{
filter,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
);
// 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">
<ArticleFilterBar onFilterUpdate={setFilter} />
<menu className={`${ARTICLE_GRID_CLASS} overflow-auto`}>
{data?.pages?.length
? allItems.map((article, idx) => (
<li key={`article-${idx}`}>
<ArticleCard {...article} />
</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>
);
}
export default InfiniteArticlesGrid;