Add infinite grid

This commit is contained in:
jeffvli
2022-07-12 23:59:32 -07:00
parent 29e8f1085a
commit 720413f4ff
3 changed files with 238 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
import { Card, Image } from '@mantine/core';
import { motion } from 'framer-motion';
import styled from 'styled-components';
const CardWrapper = styled(motion.div)<{
itemGap: number;
itemHeight: number;
itemWidth: number;
}>`
display: flex;
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
width: 100%;
height: ${({ itemHeight }) => itemHeight}px;
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682
`;
export const GridCard = ({ data, index, style }: any) => {
const { itemHeight, itemWidth, columnCount, itemGap, itemCount, itemData } =
data;
const startIndex = index * columnCount;
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
const cards = [];
for (let i = startIndex; i <= stopIndex; i += 1) {
// if (itemData[i].type === ServerType.Jellyfin) {
// const image = getJellyfinImageUrl()
// }
cards.push(
<CardWrapper
key={`card-${i}`}
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth}
whileHover={{ rotateX: 15, scale: 1.05 }}
>
<Card style={{ height: '100%', width: '100%' }}>
<Card.Section>
<Image src={itemData && itemData[i]?.image} />
</Card.Section>
</Card>
</CardWrapper>
);
}
return (
<div
style={{
...style,
alignItems: 'center',
display: 'flex',
justifyContent: 'start',
}}
>
{cards}
</div>
);
};

View File

@@ -0,0 +1,48 @@
import { Ref, useMemo } from 'react';
import { FixedSizeList, FixedSizeListProps } from 'react-window';
import { GridCard } from './GridCard';
export const VirtualGridWrapper = ({
refInstance,
itemGap,
itemWidth,
...rest
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children'> & {
itemGap: number;
itemHeight: number;
itemWidth: number;
refInstance: Ref<any>;
}) => {
const itemHeight = itemWidth + 55;
const columnCount = Math.floor(
(Number(rest.width) - itemGap + 3) / (itemWidth + itemGap + 2)
);
const rowCount = Math.ceil(rest.itemCount / columnCount);
const itemData = useMemo(
() => ({
columnCount,
itemCount: rest.itemCount,
itemData: rest.itemData,
itemGap,
itemHeight,
itemWidth,
}),
[columnCount, itemGap, itemHeight, itemWidth, rest.itemCount, rest.itemData]
);
return (
<FixedSizeList
{...rest}
ref={refInstance}
initialScrollOffset={0}
itemCount={rowCount}
itemData={itemData}
itemSize={itemHeight + itemGap}
>
{GridCard}
</FixedSizeList>
);
};

View File

@@ -0,0 +1,131 @@
import { forwardRef, Ref, useState } from 'react';
import debounce from 'lodash/debounce';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeListProps } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { VirtualGridWrapper } from './VirtualGridWrapper';
interface VirtualGridProps
extends Omit<
FixedSizeListProps,
'children' | 'itemSize' | 'height' | 'width'
> {
itemGap?: number;
itemHeight?: number;
itemWidth?: number;
minimumBatchSize?: number;
query: (props: any) => Promise<any>;
queryParams?: Record<string, any>;
}
export const VirtualInfiniteGrid = forwardRef(
(
{
itemCount,
itemGap,
itemWidth,
itemHeight,
minimumBatchSize,
query,
queryParams,
}: VirtualGridProps,
ref: Ref<InfiniteLoader>
) => {
const [itemData, setItemData] = useState<any[]>([]);
const isItemLoaded = (index: number, columnCount: number) => {
const itemIndex = index * columnCount;
return (
itemIndex < itemData.length * columnCount &&
itemData[itemIndex] !== undefined
);
};
const loadMoreItems = async (
startIndex: number,
stopIndex: number,
limit: number,
columnCount: number
) => {
const currentPage = Math.ceil(startIndex / minimumBatchSize!);
const t = await query({
limit,
page: currentPage,
...queryParams,
});
// Need to multiply by columnCount due to the grid layout
const start = startIndex * columnCount;
const end = (stopIndex + 1) * columnCount;
return new Promise<void>((resolve) => {
const newData: any[] = [...itemData];
let itemIndex = 0;
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
newData[rowIndex] = t?.data[itemIndex];
itemIndex += 1;
}
setItemData(newData);
resolve();
});
};
const debouncedLoadMoreItems = debounce(loadMoreItems, 300);
return (
<AutoSizer>
{({ height, width }) => {
const columnCount = Math.floor(
(Number(width) - itemGap! + 3) / (itemWidth! + itemGap! + 2)
);
const pageItemLimit = columnCount * minimumBatchSize!;
return (
<InfiniteLoader
ref={ref}
isItemLoaded={(index) => isItemLoaded(index, columnCount)}
itemCount={itemCount || 0}
loadMoreItems={(startIndex, stopIndex) =>
debouncedLoadMoreItems(
startIndex,
stopIndex,
pageItemLimit,
columnCount
)
}
minimumBatchSize={minimumBatchSize}
threshold={10}
>
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
<VirtualGridWrapper
height={height}
itemCount={itemCount || 0}
itemData={itemData}
itemGap={itemGap!}
itemHeight={itemHeight!}
itemWidth={itemWidth!}
refInstance={infiniteLoaderRef}
width={width}
onItemsRendered={onItemsRendered}
/>
)}
</InfiniteLoader>
);
}}
</AutoSizer>
);
}
);
VirtualInfiniteGrid.defaultProps = {
itemGap: 10,
itemHeight: 200,
itemWidth: 150,
minimumBatchSize: 20,
queryParams: {},
};