69 lines
2.0 KiB
TypeScript
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;
|