mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-19 03:11:20 +00:00
added pagination for lists
This commit is contained in:
parent
e5eff9827a
commit
f613054b9a
@ -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 doesn’t 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;
|
||||||
|
@ -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 };
|
||||||
|
};
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
97
src/components/ResourceList/VerticalPaginationList.tsx
Normal file
97
src/components/ResourceList/VerticalPaginationList.tsx
Normal 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);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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) {
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user