import { useVirtualizer } from '@tanstack/react-virtual'; import { DetailedHTMLProps, Fragment, HTMLAttributes, ReactElement, Ref, RefObject, useEffect, useRef, useState } from 'react'; import { classNames } from './classNames'; import { NitroLimitedEditionStyledNumberView } from './limited-edition'; import { styleNames } from './styleNames'; type Props = { items: T[]; columnCount: number; overscan?: number; estimateSize?: number; squareItems?: boolean; itemMinWidth?: number; rowGap?: number; itemRender?: (item: T, index?: number) => ReactElement; } const GRID_GAP_PX = 4; const useColumnMeasure = (itemMinWidth: number | null, columnCountProp: number): { parentRef: RefObject; columnCount: number } => { const parentRef = useRef(null); const [ measuredColumnCount, setMeasuredColumnCount ] = useState(columnCountProp); useEffect(() => { if(!itemMinWidth || itemMinWidth <= 0) return; const element = parentRef.current; if(!element) return; const recompute = () => { const width = element.clientWidth; const cols = Math.max(1, Math.floor((width + GRID_GAP_PX) / (itemMinWidth + GRID_GAP_PX))); setMeasuredColumnCount(prev => prev === cols ? prev : cols); }; recompute(); const observer = new ResizeObserver(recompute); observer.observe(element); return () => observer.disconnect(); }, [ itemMinWidth ]); const columnCount = (itemMinWidth && itemMinWidth > 0) ? measuredColumnCount : columnCountProp; return { parentRef, columnCount }; }; const InfiniteGridSquare = (props: Props) => { const { items = [], columnCount: columnCountProp = 4, itemMinWidth = null, itemRender = null } = props; const { parentRef } = useColumnMeasure(itemMinWidth, columnCountProp); const autoFillStyle = (itemMinWidth && itemMinWidth > 0) ? { gridTemplateColumns: `repeat(auto-fill, ${ itemMinWidth }px)` } : null; const fixedColsClass = (itemMinWidth && itemMinWidth > 0) ? '' : `grid-cols-${ columnCountProp }`; return (
{ items.map((item, index) => { if(!item) return ; return { itemRender(item, index) }; }) }
); }; const InfiniteGridVirtualized = (props: Props) => { const { items = [], columnCount: columnCountProp = 4, overscan = 5, estimateSize = 45, itemMinWidth = null, rowGap = null, itemRender = null } = props; const { parentRef, columnCount } = useColumnMeasure(itemMinWidth, columnCountProp); const rowsContainerClassName = (rowGap !== null) ? 'flex flex-col w-full relative' : 'flex flex-col w-full *:pb-1 relative'; const autoFillStyle = (itemMinWidth && itemMinWidth > 0) ? { gridTemplateColumns: `repeat(auto-fill, ${ itemMinWidth }px)` } : null; const fixedColsClass = (itemMinWidth && itemMinWidth > 0) ? '' : `grid-cols-${ columnCountProp }`; const virtualizer = useVirtualizer({ count: Math.ceil(items.length / columnCount), overscan, getScrollElement: () => parentRef.current, estimateSize: () => estimateSize }); useEffect(() => { const element = parentRef.current; if(!element || !items) return; const checkAndApplyPadding = () => { if(!element) return; element.style.paddingRight = (element.scrollHeight > element.clientHeight) ? '0.25rem' : '0'; }; checkAndApplyPadding(); window.addEventListener('resize', checkAndApplyPadding); return () => { window.removeEventListener('resize', checkAndApplyPadding); }; }, [ items, parentRef ]); useEffect(() => { if(!items || !items.length) return; virtualizer.scrollToIndex(0); }, [ items, virtualizer ]); const virtualItems = virtualizer.getVirtualItems(); return (
{ virtualItems.map(virtualRow => (
{ Array.from(Array(columnCount)).map((e, i) => { const index = (i + (virtualRow.index * columnCount)); const item = items[index]; if(!item) return ; return ( { itemRender(item, index) } ); }) }
)) }
); }; const InfiniteGridRoot = (props: Props) => { return props.squareItems ? { ...props } /> : { ...props } />; }; type InfiniteGridItemProps = { itemImage?: string; itemColor?: string; itemActive?: boolean; itemCount?: number; itemCountMinimum?: number; itemUniqueSoldout?: boolean; itemUniqueNumber?: number; itemUnseen?: boolean; itemHighlight?: boolean; disabled?: boolean; ref?: Ref; } & DetailedHTMLProps, HTMLDivElement>; const InfiniteGridItem = ({ ref, itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, itemHighlight = false, disabled = false, className = null, style = {}, children = null, ...rest }: InfiniteGridItemProps) => { const [ backgroundImageUrl, setBackgroundImageUrl ] = useState(null); const disposed = useRef(false); useEffect(() => { if(!itemImage || !itemImage.length) { setBackgroundImageUrl(null); return; } const image = new Image(); image.onload = () => { if(disposed.current) return; setBackgroundImageUrl(image.src); }; image.src = itemImage; }, [ itemImage ]); useEffect(() => { disposed.current = false; return () => { disposed.current = true; }; }, []); return (
0)) && 'unique-item', itemUniqueSoldout && 'sold-out', itemUnseen && ' animate-pulse-glow-gold border-yellow-400/60', className ) } style={ styleNames( backgroundImageUrl && backgroundImageUrl.length && !(itemUniqueSoldout || (itemUniqueNumber > 0)) && { backgroundImage: `url(${ backgroundImageUrl })` }, itemColor && { backgroundColor: itemColor }, style ) } { ...rest }> { (itemCount > itemCountMinimum) &&
{ itemCount }
} { (itemUniqueNumber > 0) && <>
} { children }
); }; export const InfiniteGrid = Object.assign(InfiniteGridRoot, { Item: InfiniteGridItem });