From 55f477957cff72285401040fd3a22a715dbe77a2 Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Mon, 17 Nov 2025 18:15:11 +0100 Subject: [PATCH] infinite scroll in items index page --- .../Templates/OverlayItemsIndexPage.tsx | 117 ++++++++++++------ 1 file changed, 80 insertions(+), 37 deletions(-) diff --git a/lib/src/Components/Templates/OverlayItemsIndexPage.tsx b/lib/src/Components/Templates/OverlayItemsIndexPage.tsx index 094ec95f..2a53c47e 100644 --- a/lib/src/Components/Templates/OverlayItemsIndexPage.tsx +++ b/lib/src/Components/Templates/OverlayItemsIndexPage.tsx @@ -39,8 +39,12 @@ export const OverlayItemsIndexPage = ({ }) => { const [loading, setLoading] = useState(false) const [addItemPopupOpen, setAddItemPopupOpen] = useState(false) + const [itemsToShow, setItemsToShow] = useState(30) + const [isLoadingMore, setIsLoadingMore] = useState(false) const tabRef = useRef(null) + const sentinelRef = useRef(null) + const scrollContainerRef = useRef(null) function scroll() { tabRef.current?.scrollIntoView() @@ -65,6 +69,65 @@ export const OverlayItemsIndexPage = ({ const layer = layers.find((l) => l.name === layerName) + // Filter and sort items once + const filteredAndSortedItems = items + .filter((i) => i.layer?.name === layerName) + .filter((item) => + filterTags.length === 0 + ? item + : filterTags.some((tag) => + getItemTags(item).some( + (filterTag) => filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase(), + ), + ), + ) + .sort((a, b) => { + const dateA = a.date_updated + ? new Date(a.date_updated).getTime() + : a.date_created + ? new Date(a.date_created).getTime() + : 0 + const dateB = b.date_updated + ? new Date(b.date_updated).getTime() + : b.date_created + ? new Date(b.date_created).getTime() + : 0 + return dateB - dateA + }) + + const visibleItems = filteredAndSortedItems.slice(0, itemsToShow) + const hasMore = filteredAndSortedItems.length > itemsToShow + + // Intersection Observer for infinite scroll + useEffect(() => { + const sentinel = sentinelRef.current + const scrollContainer = scrollContainerRef.current + if (!sentinel || !scrollContainer) return + + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0] + if (entry.isIntersecting && !isLoadingMore) { + setIsLoadingMore(true) + // Load immediately without delay for smoother UX + setItemsToShow((prev) => prev + 24) + setIsLoadingMore(false) + } + }, + { + root: scrollContainer, + rootMargin: '400px', // Start loading earlier (was 200px) + threshold: 0.1, + }, + ) + + observer.observe(sentinel) + + return () => { + observer.disconnect() + } + }, [isLoadingMore, hasMore, visibleItems.length]) + const submitNewItem = async (evt: React.FormEvent) => { evt.preventDefault() const formItem: Item = {} as Item @@ -129,44 +192,18 @@ export const OverlayItemsIndexPage = ({ -
+
- {items - .filter((i) => i.layer?.name === layerName) - .filter((item) => - filterTags.length === 0 - ? item - : filterTags.some((tag) => - getItemTags(item).some( - (filterTag) => - filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase(), - ), - ), - ) - .sort((a, b) => { - // Convert date_created to milliseconds, handle undefined by converting to lowest possible date (0 milliseconds) - const dateA = a.date_updated - ? new Date(a.date_updated).getTime() - : a.date_created - ? new Date(a.date_created).getTime() - : 0 - const dateB = b.date_updated - ? new Date(b.date_updated).getTime() - : b.date_created - ? new Date(b.date_created).getTime() - : 0 - return dateB - dateA // Subtracts milliseconds which are numbers - }) - .map((i, k) => ( -
- deleteItem(i)} - /> -
- ))} + {visibleItems.map((i, k) => ( +
+ deleteItem(i)} + /> +
+ ))} {addItemPopupOpen && (
submitNewItem(e)}>
@@ -205,6 +242,12 @@ export const OverlayItemsIndexPage = ({ )}
+ {/* Sentinel element for infinite scroll */} + {hasMore && ( +
+ {isLoadingMore && } +
+ )}