Kyoo/front/src/query/fetch-infinite.tsx
2025-06-22 21:22:37 +02:00

103 lines
2.9 KiB
TypeScript

import { LegendList } from "@legendapp/list";
import { type ComponentType, type ReactElement, useRef } from "react";
import { type Breakpoint, HR, useBreakpointMap } from "~/primitives";
import { useSetError } from "~/providers/error-provider";
import { type QueryIdentifier, useInfiniteFetch } from "~/query";
import { ErrorView } from "../ui/errors";
export type Layout = {
numColumns: Breakpoint<number>;
size: Breakpoint<number>;
gap: Breakpoint<number>;
layout: "grid" | "horizontal" | "vertical";
};
export const InfiniteFetch = <Data, Props, _, Kind extends number | string>({
query,
placeholderCount = 2,
incremental = false,
Render,
Loader,
layout,
Empty,
divider,
Header,
headerProps,
getItemType,
getItemSize,
fetchMore = true,
nested = false,
...props
}: {
query: QueryIdentifier<Data>;
placeholderCount?: number;
layout: Layout;
horizontal?: boolean;
Render: (props: { item: Data; index: number }) => ReactElement | null;
Loader: (props: { index: number }) => ReactElement | null;
Empty?: JSX.Element;
incremental?: boolean;
divider?: true | ComponentType;
Header?: ComponentType<Props & { children: JSX.Element }> | ReactElement;
headerProps?: Props;
getItemType?: (item: Data | null, index: number) => Kind;
getItemSize?: (kind: Kind) => number;
fetchMore?: boolean;
nested?: boolean;
}): JSX.Element | null => {
const { numColumns, size, gap } = useBreakpointMap(layout);
const [setOffline, clearOffline] = useSetError("offline");
const oldItems = useRef<Data[] | undefined>(undefined);
let {
items,
isPaused,
error,
fetchNextPage,
isFetching,
refetch,
isRefetching,
} = useInfiniteFetch(query);
if (incremental && items) oldItems.current = items;
if (!query.infinite)
console.warn("A non infinite query was passed to an InfiniteFetch.");
if (isPaused) setOffline();
else clearOffline();
if (error) return <ErrorView error={error} />;
if (incremental) items ??= oldItems.current;
const count = items
? numColumns - (items.length % numColumns)
: placeholderCount;
const placeholders = [...Array(count === 0 ? numColumns : count)].fill(null);
const data =
isFetching || !items ? [...(items || []), ...placeholders] : items;
return (
<LegendList
data={data}
recycleItems
renderItem={({ item, index }) =>
item ? <Render index={index} item={item} /> : <Loader index={index} />
}
keyExtractor={(item: any, index) => (item ? item.id : index)}
estimatedItemSize={size}
horizontal={layout.layout === "horizontal"}
numColumns={layout.layout === "horizontal" ? 1 : numColumns}
onEndReached={fetchMore ? () => fetchNextPage() : undefined}
onEndReachedThreshold={0.5}
onRefresh={layout.layout !== "horizontal" ? refetch : undefined}
refreshing={isRefetching}
ListHeaderComponent={Header}
ItemSeparatorComponent={
divider === true ? HR : (divider as any) || undefined
}
ListEmptyComponent={Empty}
contentContainerStyle={{ gap, marginHorizontal: gap }}
{...props}
/>
);
};