added pagination for lists

This commit is contained in:
PhilReact 2025-03-16 22:11:02 +02:00
parent e5eff9827a
commit f613054b9a
8 changed files with 264 additions and 150 deletions

View File

@ -1,22 +1,28 @@
import React, { useState, useEffect, useRef } from 'react' import { CircularProgress } from '@mui/material';
import React, { useEffect, useRef } from 'react'
import { useInView } from 'react-intersection-observer' import { useInView } from 'react-intersection-observer'
import CircularProgress from '@mui/material/CircularProgress'
interface Props { interface Props {
onLoadMore: () => void onLoadMore: () => void
} }
const LazyLoad: React.FC<Props> = ({ onLoadMore }) => { const LazyLoad: React.FC<Props> = ({ onLoadMore }) => {
const hasTriggeredRef = useRef(false); // Prevents multiple auto-triggers
const [ref, inView] = useInView({ const [ref, inView] = useInView({
threshold: 0.7 threshold: 0.7,
}) triggerOnce: false, // Allows multiple triggers, but we control when
});
useEffect(() => { useEffect(() => {
if (inView) { if (inView && !hasTriggeredRef.current) {
onLoadMore() hasTriggeredRef.current = true; // Set flag so it doesnt trigger again immediately
onLoadMore();
setTimeout(() => {
hasTriggeredRef.current = false; // Reset trigger after a short delay
}, 1000);
} }
}, [inView]) }, [inView]);
return ( return (
<div <div
@ -24,11 +30,11 @@ const LazyLoad: React.FC<Props> = ({ onLoadMore }) => {
style={{ style={{
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
minHeight: '25px' height: '50px',
overflow: 'hidden'
}} }}
> ><CircularProgress /></div>
</div>
) )
} }
export default LazyLoad export default LazyLoad;

View File

@ -1,26 +1,37 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
export const useScrollTracker = (listName: string, hasList: boolean, disableScrollTracker?: boolean) => {
const elementRef = useRef<HTMLDivElement | null>(null);
const [hasMount, setHasMount] = useState(false);
const scrollPositionRef = useRef(0); // Store the last known scroll position
export const useScrollTracker = (listName: string) => {
useEffect(() => { useEffect(() => {
if (!listName) return; if(disableScrollTracker) return
if (!listName || !hasList) return;
const SCROLL_KEY = `scroll-position-${listName}`; const SCROLL_KEY = `scroll-position-${listName}`;
// 🔹 Restore saved scroll position for the given list // 🔹 Restore scroll when the component mounts
const savedPosition = sessionStorage.getItem(SCROLL_KEY); const savedPosition = sessionStorage.getItem(SCROLL_KEY);
if (savedPosition) { if (savedPosition) {
window.scrollTo(0, parseInt(savedPosition, 10)); window.scrollTo(0, parseInt(savedPosition, 10));
setTimeout(() => {
setHasMount(true);
}, 200);
} }
// 🔹 Capture scroll position before unmount
const handleScroll = () => { const handleScroll = () => {
sessionStorage.setItem(SCROLL_KEY, window.scrollY.toString()); scrollPositionRef.current = window.scrollY; // Store the last known scroll position
}; };
// 🔹 Save scroll position on scroll
window.addEventListener("scroll", handleScroll); window.addEventListener("scroll", handleScroll);
return () => { return () => {
sessionStorage.setItem(SCROLL_KEY, scrollPositionRef.current.toString());
window.removeEventListener("scroll", handleScroll); window.removeEventListener("scroll", handleScroll);
}; };
}, [listName]); // ✅ Only runs when listName changes }, [listName, hasList, disableScrollTracker]);
};
return { elementRef, hasMount };
};

View File

@ -6,6 +6,7 @@ interface DynamicGridProps {
gap?: number; // Spacing between grid items gap?: number; // Spacing between grid items
children: ReactNode children: ReactNode
minItemWidth?: number minItemWidth?: number
setColumnsPerRow: (columns: number)=> void;
} }
const DynamicGrid: React.FC<DynamicGridProps> = ({ const DynamicGrid: React.FC<DynamicGridProps> = ({
@ -13,12 +14,32 @@ const DynamicGrid: React.FC<DynamicGridProps> = ({
minItemWidth = 200, // Minimum width per item minItemWidth = 200, // Minimum width per item
gap = 10, // Space between items gap = 10, // Space between items
children, children,
setColumnsPerRow
}) => { }) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const itemContainerRef = useRef<HTMLDivElement | null>(null);
const updateColumns = () => {
if (containerRef.current && itemContainerRef.current) {
const containerWidth = containerRef.current.clientWidth;
const itemWidth = itemContainerRef.current.clientWidth
const calculatedColumns = Math.floor(containerWidth / itemWidth);
setColumnsPerRow(calculatedColumns);
}
};
useEffect(() => {
updateColumns(); // Run on mount
window.addEventListener("resize", updateColumns);
return () => window.removeEventListener("resize", updateColumns);
}, []);
return ( return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}> <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
<div <div
ref={containerRef}
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: `repeat(auto-fill, minmax(${minItemWidth}px, 1fr))`, // ✅ Expands to fit width gridTemplateColumns: `repeat(auto-fill, minmax(${minItemWidth}px, 1fr))`, // ✅ Expands to fit width
@ -31,7 +52,7 @@ const DynamicGrid: React.FC<DynamicGridProps> = ({
}} }}
> >
{items.map((component, index) => ( {items.map((component, index) => (
<div key={index} style={{ width: "100%", display: "flex", justifyContent: "center", maxWidth: '400px' }}> <div ref={index === 0 ? itemContainerRef : null} key={index} style={{ width: "100%", display: "flex", justifyContent: "center", maxWidth: '400px' }}>
{component} {/* ✅ Renders user-provided component */} {component} {/* ✅ Renders user-provided component */}
</div> </div>
))} ))}

View File

@ -1,21 +1,24 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useMemo, useRef, useState } from "react";
import DynamicGrid from "./DynamicGrid"; import DynamicGrid from "./DynamicGrid";
import LazyLoad from "../../common/LazyLoad"; import LazyLoad from "../../common/LazyLoad";
import { ListItem, useCacheStore } from "../../state/cache"; import { ListItem } from "../../state/cache";
import { QortalMetadata } from "../../types/interfaces/resources"; import { QortalMetadata } from "../../types/interfaces/resources";
import { ListItemWrapper } from "./ResourceListDisplay"; import { DefaultLoaderParams, ListItemWrapper } from "./ResourceListDisplay";
interface HorizontalPaginatedListProps { interface HorizontalPaginatedListProps {
items: QortalMetadata[]; items: QortalMetadata[];
listItem: (item: ListItem, index: number) => React.ReactNode; listItem: (item: ListItem, index: number) => React.ReactNode;
loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode; loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode;
onLoadMore: () => void; onLoadMore: (limit: number) => void;
maxItems?: number; onLoadLess: (limit: number)=> void;
minItemWidth?: number; minItemWidth?: number;
gap?: number; gap?: number;
isLoading?: boolean; isLoading?: boolean;
onSeenLastItem?: (listItem: ListItem) => void; onSeenLastItem?: (listItem: ListItem) => void;
isLoadingMore: boolean;
limit: number,
disablePagination?: boolean
defaultLoaderParams?: DefaultLoaderParams;
} }
export const HorizontalPaginatedList = ({ export const HorizontalPaginatedList = ({
@ -23,84 +26,78 @@ export const HorizontalPaginatedList = ({
listItem, listItem,
loaderItem, loaderItem,
onLoadMore, onLoadMore,
maxItems = 60, onLoadLess,
minItemWidth, minItemWidth,
gap, gap,
isLoading, isLoading,
onSeenLastItem, onSeenLastItem,
isLoadingMore,
limit,
disablePagination,
defaultLoaderParams
}: HorizontalPaginatedListProps) => { }: HorizontalPaginatedListProps) => {
const [displayedItems, setDisplayedItems] = useState(items); const lastItemRef= useRef<any>(null)
const lastItemRef2= useRef<any>(null)
const [columnsPerRow, setColumnsPerRow] = useState<null | number>(null)
useEffect(() => { const displayedLimit = useMemo(()=> {
setDisplayedItems(items); if(disablePagination) return limit || 20
}, [items]); return Math.floor((limit || 20) / (columnsPerRow || 3)) * (columnsPerRow || 3);
}, [columnsPerRow, disablePagination])
const preserveScroll = useCallback((updateFunction: () => void) => { const displayedItems = disablePagination ? items : items?.length < (displayedLimit * 3) ? items?.slice(0, displayedLimit * 3) : items.slice(- (displayedLimit * 3))
const previousScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
const previousScrollWidth = document.documentElement.scrollWidth || document.body.scrollWidth;
updateFunction(); // Perform the update (fetch new data, remove old)
requestAnimationFrame(() => {
const newScrollWidth = document.documentElement.scrollWidth || document.body.scrollWidth;
document.documentElement.scrollLeft = document.body.scrollLeft =
previousScrollLeft - (previousScrollWidth - newScrollWidth);
});
}, []);
useEffect(() => {
if (displayedItems.length > maxItems) {
preserveScroll(() => {
const excess = displayedItems.length - maxItems;
setDisplayedItems((prev) => prev.slice(excess)); // Trim from the start
});
}
}, [displayedItems, maxItems, preserveScroll]);
useEffect(() => {
const handleScroll = () => {
const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
const clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
const scrollWidth = document.documentElement.scrollWidth || document.body.scrollWidth;
if (scrollLeft + clientWidth >= scrollWidth - 10 && !isLoading) {
onLoadMore();
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [onLoadMore, isLoading]);
return ( return (
<div style={{ overflow: "auto", width: "100%", display: "flex", flexGrow: 1 }}> <div style={{ overflowX: "hidden", width: "100%", display: "flex", flexGrow: 1, flexDirection: 'column' }}>
{!disablePagination && items?.length > (displayedLimit * 3) && (
<LazyLoad
onLoadMore={async () => {
await onLoadLess(displayedLimit);
lastItemRef2.current.scrollIntoView({ behavior: "auto", block: "start" });
setTimeout(() => {
window.scrollBy({ top: -50, behavior: "instant" }); // 'smooth' if needed
}, 0);
}}
/>
)}
<DynamicGrid <DynamicGrid
setColumnsPerRow={setColumnsPerRow}
minItemWidth={minItemWidth} minItemWidth={minItemWidth}
gap={gap} gap={gap}
items={displayedItems.map((item, index) => ( items={displayedItems?.map((item, index, list) => (
<React.Fragment key={`${item?.name}-${item?.service}-${item?.identifier}`}> <React.Fragment key={`${item?.name}-${item?.service}-${item?.identifier}`}>
<div style={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}} ref={index === displayedLimit ? lastItemRef2 : index === list.length -displayedLimit - 1 ? lastItemRef : null}>
<ListItemWrapper <ListItemWrapper
defaultLoaderParams={defaultLoaderParams}
item={item} item={item}
index={index} index={index}
render={listItem} render={listItem}
renderListItemLoader={loaderItem} renderListItemLoader={loaderItem}
/> />
</div>
</React.Fragment> </React.Fragment>
))} ))}
> >
{!isLoading && displayedItems.length > 0 && (
<LazyLoad <LazyLoad
onLoadMore={() => { onLoadMore={async () => {
onLoadMore(); await onLoadMore(displayedLimit);
if (onSeenLastItem) { lastItemRef.current.scrollIntoView({ behavior: "auto", block: "end" });
// onSeenLastItem(displayedItems[displayedItems.length - 1]); setTimeout(() => {
} window.scrollBy({ top: 50, behavior: "instant" }); // 'smooth' if needed
}, 0);
}} }}
/> />
)}
</DynamicGrid> </DynamicGrid>
</div> </div>
); );
}; };

View File

@ -2,28 +2,25 @@ import React, {
CSSProperties, CSSProperties,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
useTransition,
} from "react"; } from "react";
import { import {
QortalMetadata, QortalMetadata,
QortalSearchParams, QortalSearchParams,
} from "../../types/interfaces/resources"; } from "../../types/interfaces/resources";
import { useResources } from "../../hooks/useResources"; import { useResources } from "../../hooks/useResources";
import { MessageWrapper, VirtualizedList } from "../../common/VirtualizedList"; import { VirtualizedList } from "../../common/VirtualizedList";
import { ListLoader } from "../../common/ListLoader"; import { ListLoader } from "../../common/ListLoader";
import { ListItem, useCacheStore } from "../../state/cache"; import { ListItem, useCacheStore } from "../../state/cache";
import { ResourceLoader } from "./ResourceLoader"; import { ResourceLoader } from "./ResourceLoader";
import { ItemCardWrapper } from "./ItemCardWrapper"; import { ItemCardWrapper } from "./ItemCardWrapper";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import DynamicGrid from "./DynamicGrid";
import LazyLoad from "../../common/LazyLoad";
import { useListStore } from "../../state/lists"; import { useListStore } from "../../state/lists";
import { useScrollTracker } from "../../common/useScrollTracker"; import { useScrollTracker } from "../../common/useScrollTracker";
import { HorizontalPaginatedList } from "./HorizontalPaginationList"; import { HorizontalPaginatedList } from "./HorizontalPaginationList";
import { VerticalPaginatedList } from "./VerticalPaginationList";
type Direction = "VERTICAL" | "HORIZONTAL"; type Direction = "VERTICAL" | "HORIZONTAL";
interface ResourceListStyles { interface ResourceListStyles {
@ -37,7 +34,7 @@ interface ResourceListStyles {
} }
} }
interface DefaultLoaderParams { export interface DefaultLoaderParams {
listLoadingText?: string; listLoadingText?: string;
listNoResultsText?: string; listNoResultsText?: string;
listItemLoadingText?: string; listItemLoadingText?: string;
@ -57,6 +54,8 @@ interface BaseProps {
children?: React.ReactNode; children?: React.ReactNode;
searchCacheDuration?: number searchCacheDuration?: number
resourceCacheDuration?: number resourceCacheDuration?: number
disablePagination?: boolean
disableScrollTracker?: boolean
} }
// ✅ Restrict `direction` only when `disableVirtualization = false` // ✅ Restrict `direction` only when `disableVirtualization = false`
@ -86,17 +85,22 @@ export const MemorizedComponent = ({
onSeenLastItem, onSeenLastItem,
listName, listName,
searchCacheDuration, searchCacheDuration,
resourceCacheDuration resourceCacheDuration,
disablePagination,
disableScrollTracker
}: PropsResourceListDisplay) => { }: PropsResourceListDisplay) => {
const { fetchResources } = useResources(); const { fetchResources } = useResources();
const { getTemporaryResources, filterOutDeletedResources } = useCacheStore(); const { getTemporaryResources, filterOutDeletedResources } = useCacheStore();
const [isLoading, setIsLoading] = useState(false);
const memoizedParams = useMemo(() => JSON.stringify(search), [search]); const memoizedParams = useMemo(() => JSON.stringify(search), [search]);
const addList = useListStore().addList const addList = useListStore().addList
const removeFromList = useListStore().removeFromList
const addItems = useListStore().addItems const addItems = useListStore().addItems
const getListByName = useListStore().getListByName
const list = useListStore().getListByName(listName) const list = useListStore().getListByName(listName)
const [isLoading, setIsLoading] = useState(list?.length > 0 ? false : true);
const isListExpired = useCacheStore().isListExpired(listName) const isListExpired = useCacheStore().isListExpired(listName)
const [isLoadingMore, setIsLoadingMore] = useState(false)
const initialized = useRef(false) const initialized = useRef(false)
const getResourceList = useCallback(async () => { const getResourceList = useCallback(async () => {
@ -124,13 +128,16 @@ export const MemorizedComponent = ({
useEffect(() => { useEffect(() => {
if(initialized.current) return if(initialized.current) return
initialized.current = true initialized.current = true
if(!isListExpired) return if(!isListExpired) {
setIsLoading(false)
return
}
sessionStorage.removeItem(`scroll-position-${listName}`); sessionStorage.removeItem(`scroll-position-${listName}`);
getResourceList(); getResourceList();
}, [getResourceList, isListExpired]); // Runs when dependencies change }, [getResourceList, isListExpired]); // Runs when dependencies change
useScrollTracker(listName); const {elementRef} = useScrollTracker(listName, list?.length > 0, disableScrollTracker);
const setSearchCacheExpiryDuration = useCacheStore().setSearchCacheExpiryDuration const setSearchCacheExpiryDuration = useCacheStore().setSearchCacheExpiryDuration
const setResourceCacheExpiryDuration = useCacheStore().setResourceCacheExpiryDuration const setResourceCacheExpiryDuration = useCacheStore().setResourceCacheExpiryDuration
@ -152,18 +159,24 @@ export const MemorizedComponent = ({
const getResourceMoreList = useCallback(async () => { const getResourceMoreList = useCallback(async (displayLimit?: number) => {
try { try {
// setIsLoading(true); setIsLoadingMore(true)
const parsedParams = {...(JSON.parse(memoizedParams))}; const parsedParams = {...(JSON.parse(memoizedParams))};
parsedParams.before = list.length === 0 ? null : list[list.length - 1]?.created parsedParams.before = list.length === 0 ? null : list[list.length - 1]?.created
parsedParams.offset = null parsedParams.offset = null
if(displayLimit){
parsedParams.limit = displayLimit
}
const responseData = await fetchResources(parsedParams, listName); // Awaiting the async function const responseData = await fetchResources(parsedParams, listName); // Awaiting the async function
addItems(listName, responseData || []) addItems(listName, responseData || [])
} catch (error) { } catch (error) {
console.error("Failed to fetch resources:", error); console.error("Failed to fetch resources:", error);
} finally { } finally {
setIsLoading(false); setTimeout(() => {
setIsLoadingMore(false);
}, 1000);
} }
}, [memoizedParams, listName, list]); }, [memoizedParams, listName, list]);
@ -191,6 +204,10 @@ export const MemorizedComponent = ({
}, [listName]); }, [listName]);
return ( return (
<div ref={elementRef} style={{
width: '100%',
height: '100%'
}}>
<ListLoader <ListLoader
noResultsMessage={ noResultsMessage={
defaultLoaderParams?.listNoResultsText || "No results available" defaultLoaderParams?.listNoResultsText || "No results available"
@ -204,6 +221,7 @@ export const MemorizedComponent = ({
loaderHeight={styles?.listLoadingHeight} loaderHeight={styles?.listLoadingHeight}
> >
<div <div
style={{ style={{
height: "100%", height: "100%",
display: "flex", display: "flex",
@ -236,72 +254,24 @@ export const MemorizedComponent = ({
)} )}
{disableVirtualization && direction === "HORIZONTAL" && ( {disableVirtualization && direction === "HORIZONTAL" && (
<> <>
<DynamicGrid <HorizontalPaginatedList defaultLoaderParams={defaultLoaderParams} disablePagination={disablePagination} limit={search?.limit || 20} onLoadLess={(displayLimit)=> {
minItemWidth={styles?.horizontalStyles?.minItemWidth} removeFromList(listName, displayLimit)
gap={styles?.gap} }} isLoadingMore={isLoadingMore} items={listToDisplay} listItem={listItem} onLoadMore={(displayLimit)=> getResourceMoreList(displayLimit)} gap={styles?.gap} isLoading={isLoading} minItemWidth={styles?.horizontalStyles?.minItemWidth} loaderItem={loaderItem} />
items={listToDisplay?.map((item, index) => {
return (
<React.Fragment
key={`${item?.name}-${item?.service}-${item?.identifier}`}
>
<ListItemWrapper
defaultLoaderParams={defaultLoaderParams}
item={item}
index={index}
render={listItem}
renderListItemLoader={loaderItem}
/>
</React.Fragment>
);
})}
>
{!isLoading && listToDisplay?.length > 0 && (
<LazyLoad onLoadMore={()=> {
getResourceMoreList()
if(onSeenLastItem){
onSeenLastItem(listToDisplay[listToDisplay?.length - 1])
}
}} />
)}
</DynamicGrid>
</> </>
)} )}
{disableVirtualization && direction === "VERTICAL" && ( {disableVirtualization && direction === "VERTICAL" && (
<div style={disabledVirutalizationStyles}> <div style={disabledVirutalizationStyles}>
{listToDisplay?.map((item, index) => { <VerticalPaginatedList disablePagination={disablePagination} limit={search?.limit || 20} onLoadLess={(displayLimit)=> {
return (
<React.Fragment removeFromList(listName, displayLimit)
key={`${item?.name}-${item?.service}-${item?.identifier}`} }} defaultLoaderParams={defaultLoaderParams} isLoadingMore={isLoadingMore} items={listToDisplay} listItem={listItem} onLoadMore={(displayLimit)=> getResourceMoreList(displayLimit)} gap={styles?.gap} isLoading={isLoading} minItemWidth={styles?.horizontalStyles?.minItemWidth} loaderItem={loaderItem} />
>
<ListItemWrapper
defaultLoaderParams={defaultLoaderParams}
item={item}
index={index}
render={listItem}
renderListItemLoader={loaderItem}
/>
</React.Fragment>
);
})}
{!isLoading && listToDisplay?.length > 0 && (
<LazyLoad onLoadMore={()=> {
getResourceMoreList()
if(onSeenLastItem){
onSeenLastItem(listToDisplay[listToDisplay?.length - 1])
}
}} />
)}
</div> </div>
)} )}
</div> </div>
</div> </div>
</ListLoader> </ListLoader>
</div>
); );
} }

View File

@ -0,0 +1,97 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import DynamicGrid from "./DynamicGrid";
import LazyLoad from "../../common/LazyLoad";
import { ListItem } from "../../state/cache";
import { QortalMetadata } from "../../types/interfaces/resources";
import { DefaultLoaderParams, ListItemWrapper } from "./ResourceListDisplay";
import { Button } from "@mui/material";
interface VerticalPaginatedListProps {
items: QortalMetadata[];
listItem: (item: ListItem, index: number) => React.ReactNode;
loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode;
onLoadMore: (limit: number) => void;
onLoadLess: (limit: number)=> void;
minItemWidth?: number;
gap?: number;
isLoading?: boolean;
onSeenLastItem?: (listItem: ListItem) => void;
isLoadingMore: boolean;
limit: number,
disablePagination?: boolean
defaultLoaderParams?: DefaultLoaderParams;
}
export const VerticalPaginatedList = ({
items,
listItem,
loaderItem,
onLoadMore,
onLoadLess,
minItemWidth,
gap,
isLoading,
onSeenLastItem,
isLoadingMore,
limit,
disablePagination,
defaultLoaderParams
}: VerticalPaginatedListProps) => {
const lastItemRef= useRef<any>(null)
const lastItemRef2= useRef<any>(null)
const displayedLimit = limit || 20
const displayedItems = disablePagination ? items : items.slice(- (displayedLimit * 3))
return (
<>
{!disablePagination && items?.length > (displayedLimit * 3) && (
<LazyLoad
onLoadMore={async () => {
await onLoadLess(displayedLimit);
lastItemRef2.current.scrollIntoView({ behavior: "auto", block: "start" });
setTimeout(() => {
window.scrollBy({ top: -50, behavior: "instant" }); // 'smooth' if needed
}, 0);
}}
/>
)}
{displayedItems?.map((item, index, list) => {
return (
<React.Fragment
key={`${item?.name}-${item?.service}-${item?.identifier}`}
>
<div style={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}} ref={index === displayedLimit ? lastItemRef2 : index === list.length -displayedLimit - 1 ? lastItemRef : null}>
<ListItemWrapper
defaultLoaderParams={defaultLoaderParams}
item={item}
index={index}
render={listItem}
renderListItemLoader={loaderItem}
/>
</div>
</React.Fragment>
);
})}
<LazyLoad
onLoadMore={async () => {
await onLoadMore(displayedLimit);
lastItemRef.current.scrollIntoView({ behavior: "auto", block: "end" });
setTimeout(() => {
window.scrollBy({ top: 50, behavior: "instant" }); // 'smooth' if needed
}, 0);
}}
/>
</>
);
};

View File

@ -29,6 +29,7 @@ export const useResources = () => {
} = useCacheStore(); } = useCacheStore();
const requestControllers = new Map<string, AbortController>(); const requestControllers = new Map<string, AbortController>();
const getArbitraryResource = async ( const getArbitraryResource = async (
url: string, url: string,
key: string key: string
@ -42,6 +43,7 @@ export const useResources = () => {
try { try {
const res = await fetch(url, { signal: controller.signal }); const res = await fetch(url, { signal: controller.signal });
if(!res?.ok) throw new Error('Error in downloading')
return await res.text(); return await res.text();
} catch (error: any) { } catch (error: any) {
if (error?.name === "AbortError") { if (error?.name === "AbortError") {
@ -90,6 +92,8 @@ export const useResources = () => {
} catch (error) { } catch (error) {
hasFailedToDownload = true; hasFailedToDownload = true;
} }
if (res === "canceled") return false; if (res === "canceled") return false;
if (hasFailedToDownload) { if (hasFailedToDownload) {

View File

@ -15,6 +15,7 @@ interface ListStore {
// CRUD Operations // CRUD Operations
addList: (name: string, items: QortalMetadata[]) => void; addList: (name: string, items: QortalMetadata[]) => void;
removeFromList: (name: string, length: number)=> void;
addItem: (listName: string, item: QortalMetadata) => void; addItem: (listName: string, item: QortalMetadata) => void;
addItems: (listName: string, items: QortalMetadata[]) => void; addItems: (listName: string, items: QortalMetadata[]) => void;
updateItem: (listName: string, item: QortalMetadata) => void; updateItem: (listName: string, item: QortalMetadata) => void;
@ -35,6 +36,13 @@ export const useListStore = create<ListStore>((set, get) => ({
[name]: { name, items }, // ✅ Store items as an array [name]: { name, items }, // ✅ Store items as an array
}, },
})), })),
removeFromList: (name, length) =>
set((state) => ({
lists: {
...state.lists,
[name]: { name, items: state.lists[name].items.slice(0, state.lists[name].items.length - length) }, // ✅ Store items as an array
},
})),
addItem: (listName, item) => addItem: (listName, item) =>
set((state) => { set((state) => {