mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-15 01:41:21 +00:00
updated lists
This commit is contained in:
parent
87a4f891a0
commit
bb50cfc9a2
34
src/common/LazyLoad.tsx
Normal file
34
src/common/LazyLoad.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
import { useInView } from 'react-intersection-observer'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onLoadMore: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LazyLoad: React.FC<Props> = ({ onLoadMore }) => {
|
||||||
|
|
||||||
|
const [ref, inView] = useInView({
|
||||||
|
threshold: 0.7
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inView) {
|
||||||
|
onLoadMore()
|
||||||
|
}
|
||||||
|
}, [inView])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: '25px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LazyLoad
|
@ -31,12 +31,12 @@ export const VirtualizedList = ({ list, children, onSeenLastItem }: PropsVirtual
|
|||||||
overscan: 10, // Number of items to render outside the visible area to improve smoothness
|
overscan: 10, // Number of items to render outside the visible area to improve smoothness
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSeenLastItemFunc = useCallback((lastItem: QortalMetadata) => {
|
const onSeenLastItemFunc = (lastItem: QortalMetadata) => {
|
||||||
if(onSeenLastItem){
|
if(onSeenLastItem){
|
||||||
onSeenLastItem(lastItem)
|
onSeenLastItem(lastItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
64
src/components/ResourceList/DynamicGrid.tsx
Normal file
64
src/components/ResourceList/DynamicGrid.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useEffect, useRef, useState, ReactNode } from "react";
|
||||||
|
|
||||||
|
interface DynamicGridProps {
|
||||||
|
items: ReactNode[]; // ✅ Accepts an array of JSX components
|
||||||
|
itemWidth?: number; // Minimum width per item
|
||||||
|
gap?: number; // Spacing between grid items
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const DynamicGrid: React.FC<DynamicGridProps> = ({
|
||||||
|
items,
|
||||||
|
itemWidth = 300, // Default min item width
|
||||||
|
gap = 10, // Default gap between items
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
const gridRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [columns, setColumns] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateColumns = () => {
|
||||||
|
if (gridRef.current) {
|
||||||
|
const containerWidth = gridRef.current.offsetWidth;
|
||||||
|
const newColumns = Math.floor(containerWidth / (itemWidth + gap));
|
||||||
|
setColumns(newColumns > 0 ? newColumns : 1); // Ensure at least 1 column
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateColumns(); // Initial column calculation
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(updateColumns);
|
||||||
|
if (gridRef.current) {
|
||||||
|
resizeObserver.observe(gridRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect(); // Cleanup observer
|
||||||
|
}, [itemWidth, gap]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: 'column', alignItems: "center", width: "100%" }}>
|
||||||
|
{/* ✅ Centers the grid inside the parent */}
|
||||||
|
<div
|
||||||
|
ref={gridRef}
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: `repeat(${columns}, minmax(${itemWidth}px, 1fr))`, // ✅ Dynamically calculated columns
|
||||||
|
gap: `${gap}px`,
|
||||||
|
width: "100%", // ✅ Ensures grid fills available space
|
||||||
|
maxWidth: "1200px", // ✅ Optional max width to prevent excessive stretching
|
||||||
|
margin: "auto", // ✅ Centers the grid horizontally
|
||||||
|
gridAutoFlow: "row", // ✅ Ensures rows behave correctly
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items.map((component, index) => (
|
||||||
|
<div key={index} style={{ width: "100%", display: "flex", justifyContent: "center" }}>
|
||||||
|
{component} {/* ✅ Render user-provided component */}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicGrid;
|
@ -10,16 +10,23 @@ import {
|
|||||||
QortalSearchParams,
|
QortalSearchParams,
|
||||||
} from "../../types/interfaces/resources";
|
} from "../../types/interfaces/resources";
|
||||||
import { useResources } from "../../hooks/useResources";
|
import { useResources } from "../../hooks/useResources";
|
||||||
import { VirtualizedList } from "../../common/VirtualizedList";
|
import { MessageWrapper, 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";
|
||||||
|
type Direction = "VERTICAL" | "HORIZONTAL";
|
||||||
|
|
||||||
interface ResourceListStyles {
|
interface ResourceListStyles {
|
||||||
gap?: number;
|
gap?: number;
|
||||||
listLoadingHeight?: CSSProperties;
|
listLoadingHeight?: CSSProperties;
|
||||||
|
disabledVirutalizationStyles?: {
|
||||||
|
parentContainer?: CSSProperties;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DefaultLoaderParams {
|
interface DefaultLoaderParams {
|
||||||
@ -29,17 +36,31 @@ interface DefaultLoaderParams {
|
|||||||
listItemErrorText?: string;
|
listItemErrorText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PropsResourceListDisplay {
|
interface BaseProps {
|
||||||
params: QortalSearchParams;
|
params: QortalSearchParams;
|
||||||
listItem: (item: ListItem, index: number) => React.ReactNode; // Function type
|
listItem: (item: ListItem, index: number) => React.ReactNode;
|
||||||
styles?: ResourceListStyles;
|
styles?: ResourceListStyles;
|
||||||
loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode; // Function type
|
loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode;
|
||||||
defaultLoaderParams?: DefaultLoaderParams;
|
defaultLoaderParams?: DefaultLoaderParams;
|
||||||
loaderList?: (status: "LOADING" | "NO_RESULTS") => React.ReactNode; // Function type
|
loaderList?: (status: "LOADING" | "NO_RESULTS") => React.ReactNode;
|
||||||
disableVirtualization?: boolean;
|
disableVirtualization?: boolean;
|
||||||
onSeenLastItem?: (listItem: QortalMetadata)=> void;
|
onSeenLastItem?: (listItem: QortalMetadata) => void;
|
||||||
|
listName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Restrict `direction` only when `disableVirtualization = false`
|
||||||
|
interface VirtualizedProps extends BaseProps {
|
||||||
|
disableVirtualization?: false;
|
||||||
|
direction?: "VERTICAL"; // Only allow "VERTICAL" when virtualization is enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NonVirtualizedProps extends BaseProps {
|
||||||
|
disableVirtualization: true;
|
||||||
|
direction?: Direction; // Allow both "VERTICAL" & "HORIZONTAL" when virtualization is disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
type PropsResourceListDisplay = VirtualizedProps | NonVirtualizedProps;
|
||||||
|
|
||||||
export const ResourceListDisplay = ({
|
export const ResourceListDisplay = ({
|
||||||
params,
|
params,
|
||||||
listItem,
|
listItem,
|
||||||
@ -50,19 +71,23 @@ export const ResourceListDisplay = ({
|
|||||||
loaderItem,
|
loaderItem,
|
||||||
loaderList,
|
loaderList,
|
||||||
disableVirtualization,
|
disableVirtualization,
|
||||||
onSeenLastItem
|
direction = "VERTICAL",
|
||||||
|
onSeenLastItem,
|
||||||
|
listName
|
||||||
}: PropsResourceListDisplay) => {
|
}: PropsResourceListDisplay) => {
|
||||||
const [list, setList] = useState<QortalMetadata[]>([]);
|
|
||||||
const { fetchResources } = useResources();
|
const { fetchResources } = useResources();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const memoizedParams = useMemo(() => JSON.stringify(params), [params]);
|
const memoizedParams = useMemo(() => JSON.stringify(params), [params]);
|
||||||
|
const addList = useListStore().addList
|
||||||
|
const addItems = useListStore().addItems
|
||||||
|
const list = useListStore().getListByName(listName)
|
||||||
|
|
||||||
const getResourceList = useCallback(async () => {
|
const getResourceList = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const parsedParams = JSON.parse(memoizedParams);
|
const parsedParams = JSON.parse(memoizedParams);
|
||||||
const res = await fetchResources(parsedParams); // Awaiting the async function
|
const res = await fetchResources(parsedParams, listName, true); // Awaiting the async function
|
||||||
setList(res || []); // Ensure it's an array, avoid setting `undefined`
|
addList(listName, res || [])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch resources:", error);
|
console.error("Failed to fetch resources:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -70,10 +95,36 @@ export const ResourceListDisplay = ({
|
|||||||
}
|
}
|
||||||
}, [memoizedParams, fetchResources]); // Added dependencies for re-fetching
|
}, [memoizedParams, fetchResources]); // Added dependencies for re-fetching
|
||||||
|
|
||||||
|
const getResourceMoreList = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
// setIsLoading(true);
|
||||||
|
const parsedParams = {...(JSON.parse(memoizedParams))};
|
||||||
|
parsedParams.offset = (parsedParams?.offset || 0) + list.length
|
||||||
|
const res = await fetchResources(parsedParams, listName); // Awaiting the async function
|
||||||
|
addItems(listName, res || [])
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch resources:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [memoizedParams, listName, list?.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getResourceList();
|
getResourceList();
|
||||||
}, [getResourceList]); // Runs when dependencies change
|
}, [getResourceList]); // Runs when dependencies change
|
||||||
|
|
||||||
|
const disabledVirutalizationStyles: CSSProperties = useMemo(() => {
|
||||||
|
if (styles?.disabledVirutalizationStyles?.parentContainer)
|
||||||
|
return styles?.disabledVirutalizationStyles.parentContainer;
|
||||||
|
return {
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${styles.gap}px` || 0,
|
||||||
|
width: "100%",
|
||||||
|
};
|
||||||
|
}, [styles?.disabledVirutalizationStyles, styles?.gap, direction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListLoader
|
<ListLoader
|
||||||
noResultsMessage={
|
noResultsMessage={
|
||||||
@ -96,10 +147,15 @@ export const ResourceListDisplay = ({
|
|||||||
>
|
>
|
||||||
<div style={{ display: "flex", flexGrow: 1 }}>
|
<div style={{ display: "flex", flexGrow: 1 }}>
|
||||||
{!disableVirtualization && (
|
{!disableVirtualization && (
|
||||||
<VirtualizedList list={list} onSeenLastItem={onSeenLastItem}>
|
<VirtualizedList list={list} onSeenLastItem={(item)=> {
|
||||||
|
getResourceMoreList()
|
||||||
|
if(onSeenLastItem){
|
||||||
|
onSeenLastItem(item)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
{(item: QortalMetadata, index: number) => (
|
{(item: QortalMetadata, index: number) => (
|
||||||
<>
|
<>
|
||||||
{styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}
|
{styles?.gap && <Spacer height={`${styles.gap / 2}px`} />}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ListItemWrapper
|
<ListItemWrapper
|
||||||
defaultLoaderParams={defaultLoaderParams}
|
defaultLoaderParams={defaultLoaderParams}
|
||||||
@ -108,27 +164,20 @@ export const ResourceListDisplay = ({
|
|||||||
render={listItem}
|
render={listItem}
|
||||||
renderListItemLoader={loaderItem}
|
renderListItemLoader={loaderItem}
|
||||||
/>
|
/>
|
||||||
{styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}
|
{styles?.gap && <Spacer height={`${styles.gap / 2}px`} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</VirtualizedList>
|
</VirtualizedList>
|
||||||
)}
|
)}
|
||||||
{disableVirtualization && (
|
{disableVirtualization && direction === "HORIZONTAL" && (
|
||||||
<div
|
<>
|
||||||
style={{
|
<DynamicGrid
|
||||||
position: "relative",
|
gap={styles?.gap}
|
||||||
display: "flex",
|
items={list?.map((item, index) => {
|
||||||
flexDirection: "column",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{list?.map((item, index) => {
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment
|
<React.Fragment
|
||||||
key={`${item?.name}-${item?.service}-${item?.identifier}`}
|
key={`${item?.name}-${item?.service}-${item?.identifier}`}
|
||||||
>
|
>
|
||||||
{styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}
|
|
||||||
<Spacer />
|
|
||||||
<ListItemWrapper
|
<ListItemWrapper
|
||||||
defaultLoaderParams={defaultLoaderParams}
|
defaultLoaderParams={defaultLoaderParams}
|
||||||
item={item}
|
item={item}
|
||||||
@ -136,10 +185,52 @@ export const ResourceListDisplay = ({
|
|||||||
render={listItem}
|
render={listItem}
|
||||||
renderListItemLoader={loaderItem}
|
renderListItemLoader={loaderItem}
|
||||||
/>
|
/>
|
||||||
{styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
>
|
||||||
|
|
||||||
|
{!isLoading && list?.length > 0 && (
|
||||||
|
<LazyLoad onLoadMore={()=> {
|
||||||
|
getResourceMoreList()
|
||||||
|
if(onSeenLastItem){
|
||||||
|
|
||||||
|
onSeenLastItem(list[list?.length - 1])
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</DynamicGrid>
|
||||||
|
</>
|
||||||
|
|
||||||
|
)}
|
||||||
|
{disableVirtualization && direction === "VERTICAL" && (
|
||||||
|
<div style={disabledVirutalizationStyles}>
|
||||||
|
{list?.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 && list?.length > 0 && (
|
||||||
|
<LazyLoad onLoadMore={()=> {
|
||||||
|
getResourceMoreList()
|
||||||
|
if(onSeenLastItem){
|
||||||
|
onSeenLastItem(list[list?.length - 1])
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,10 +9,45 @@ export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(5)
|
|||||||
|
|
||||||
export const useResources = () => {
|
export const useResources = () => {
|
||||||
const { setSearchCache, getSearchCache, getResourceCache, setResourceCache } = useCacheStore();
|
const { setSearchCache, getSearchCache, getResourceCache, setResourceCache } = useCacheStore();
|
||||||
|
const requestControllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
const getArbitraryResource = async (url: string, key: string): Promise<string> => {
|
||||||
|
// ✅ Create or reuse an existing controller
|
||||||
|
let controller = requestControllers.get(key);
|
||||||
|
if (!controller) {
|
||||||
|
controller = new AbortController();
|
||||||
|
requestControllers.set(key, controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { signal: controller.signal });
|
||||||
|
return await res.text();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.name === "AbortError") {
|
||||||
|
console.warn(`Request cancelled: ${key}`);
|
||||||
|
return "canceled"; // Return empty response on cancel
|
||||||
|
} else {
|
||||||
|
console.error(`Fetch error: ${key}`, error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
requestControllers.delete(key); // ✅ Cleanup controller after request
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelAllRequests = () => {
|
||||||
|
requestControllers.forEach((controller, key) => {
|
||||||
|
controller.abort();
|
||||||
|
});
|
||||||
|
requestControllers.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const fetchIndividualPublish = useCallback(
|
const fetchIndividualPublish = useCallback(
|
||||||
async (item: QortalMetadata) => {
|
async (item: QortalMetadata) => {
|
||||||
try {
|
try {
|
||||||
|
const key = `${item?.service}-${item?.name}-${item?.identifier}`;
|
||||||
|
|
||||||
const cachedProduct = getResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`);
|
const cachedProduct = getResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`);
|
||||||
if (cachedProduct) return;
|
if (cachedProduct) return;
|
||||||
setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, null);
|
setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, null);
|
||||||
@ -20,17 +55,12 @@ export const useResources = () => {
|
|||||||
let res: string | undefined = undefined
|
let res: string | undefined = undefined
|
||||||
try {
|
try {
|
||||||
res = await requestQueueProductPublishes.enqueue((): Promise<string> => {
|
res = await requestQueueProductPublishes.enqueue((): Promise<string> => {
|
||||||
return qortalRequest({
|
return getArbitraryResource(`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, key)
|
||||||
action: "FETCH_QDN_RESOURCE",
|
|
||||||
identifier: item?.identifier,
|
|
||||||
encoding: "base64",
|
|
||||||
name: item?.name,
|
|
||||||
service: item?.service,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hasFailedToDownload = true
|
hasFailedToDownload = true
|
||||||
}
|
}
|
||||||
|
if(res === 'canceled') return false
|
||||||
|
|
||||||
if (hasFailedToDownload) {
|
if (hasFailedToDownload) {
|
||||||
await new Promise((res) => {
|
await new Promise((res) => {
|
||||||
@ -41,13 +71,7 @@ export const useResources = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
res = await requestQueueProductPublishesBackup.enqueue((): Promise<string> => {
|
res = await requestQueueProductPublishesBackup.enqueue((): Promise<string> => {
|
||||||
return qortalRequest({
|
return getArbitraryResource(`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, key)
|
||||||
action: "FETCH_QDN_RESOURCE",
|
|
||||||
identifier: item?.identifier,
|
|
||||||
encoding: "base64",
|
|
||||||
name: item?.name,
|
|
||||||
service: item?.service,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, false);
|
setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, false);
|
||||||
@ -79,9 +103,17 @@ export const useResources = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fetchResources = useCallback(
|
const fetchResources = useCallback(
|
||||||
async (params: QortalSearchParams): Promise<QortalMetadata[]> => {
|
async (params: QortalSearchParams, listName: string, cancelRequests?: boolean): Promise<QortalMetadata[]> => {
|
||||||
|
if(cancelRequests){
|
||||||
|
cancelAllRequests()
|
||||||
|
await new Promise((res)=> {
|
||||||
|
setTimeout(() => {
|
||||||
|
res(null)
|
||||||
|
}, 250);
|
||||||
|
})
|
||||||
|
}
|
||||||
const cacheKey = generateCacheKey(params);
|
const cacheKey = generateCacheKey(params);
|
||||||
const searchCache = getSearchCache(cacheKey);
|
const searchCache = getSearchCache(listName, cacheKey);
|
||||||
let responseData = [];
|
let responseData = [];
|
||||||
|
|
||||||
if (searchCache) {
|
if (searchCache) {
|
||||||
@ -96,7 +128,7 @@ export const useResources = () => {
|
|||||||
if (!response) throw new Error("Unable to fetch resources");
|
if (!response) throw new Error("Unable to fetch resources");
|
||||||
responseData = response
|
responseData = response
|
||||||
}
|
}
|
||||||
setSearchCache(cacheKey, responseData);
|
setSearchCache(listName, cacheKey, responseData);
|
||||||
fetchDataFromResults(responseData);
|
fetchDataFromResults(responseData);
|
||||||
|
|
||||||
return responseData;
|
return responseData;
|
||||||
|
@ -3,13 +3,16 @@ import { QortalMetadata } from "../types/interfaces/resources";
|
|||||||
|
|
||||||
|
|
||||||
interface SearchCache {
|
interface SearchCache {
|
||||||
[searchTerm: string]: {
|
[listName: string]: {
|
||||||
data: QortalMetadata[]; // List of products for the search term
|
searches: {
|
||||||
expiry: number; // Expiry timestamp in milliseconds
|
[searchTerm: string]: QortalMetadata[]; // List of products for each search term
|
||||||
|
};
|
||||||
|
expiry: number; // Expiry timestamp for the whole list
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const mergeUniqueItems = (array1: QortalMetadata[], array2: QortalMetadata[]) => {
|
export const mergeUniqueItems = (array1: QortalMetadata[], array2: QortalMetadata[]) => {
|
||||||
const mergedArray = [...array1, ...array2];
|
const mergedArray = [...array1, ...array2];
|
||||||
|
|
||||||
@ -46,8 +49,8 @@ interface CacheState {
|
|||||||
// Search cache actions
|
// Search cache actions
|
||||||
setResourceCache: (id: string, data: ListItem | false | null, customExpiry?: number) => void;
|
setResourceCache: (id: string, data: ListItem | false | null, customExpiry?: number) => void;
|
||||||
|
|
||||||
setSearchCache: (searchTerm: string, data: QortalMetadata[], customExpiry?: number) => void;
|
setSearchCache: (listName: string, searchTerm: string, data: QortalMetadata[], customExpiry?: number) => void;
|
||||||
getSearchCache: (searchTerm: string) => QortalMetadata[] | null;
|
getSearchCache: (listName: string, searchTerm: string) => QortalMetadata[] | null;
|
||||||
clearExpiredCache: () => void;
|
clearExpiredCache: () => void;
|
||||||
getResourceCache: (id: string, ignoreExpire?: boolean) => ListItem | false | null;
|
getResourceCache: (id: string, ignoreExpire?: boolean) => ListItem | false | null;
|
||||||
}
|
}
|
||||||
@ -76,22 +79,30 @@ export const useCacheStore = create<CacheState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
// Add search results to cache
|
// Add search results to cache
|
||||||
setSearchCache: (searchTerm, data, customExpiry) =>
|
setSearchCache: (listName, searchTerm, data, customExpiry) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const expiry = Date.now() + (customExpiry || (5 * 60 * 1000)); // 5mins from now
|
const expiry = Date.now() + (customExpiry || 5 * 60 * 1000); // 5mins from now
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchCache: {
|
searchCache: {
|
||||||
...state.searchCache,
|
...state.searchCache,
|
||||||
[searchTerm]: { data, expiry },
|
[listName]: {
|
||||||
|
searches: {
|
||||||
|
...(state.searchCache[listName]?.searches || {}), // Preserve existing searches
|
||||||
|
[searchTerm]: data, // Store new search term results
|
||||||
|
},
|
||||||
|
expiry, // Expiry for the entire list
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
// Retrieve cached search results
|
// Retrieve cached search results
|
||||||
getSearchCache: (searchTerm) => {
|
getSearchCache: (listName, searchTerm) => {
|
||||||
const cache = get().searchCache[searchTerm];
|
const cache = get().searchCache[listName];
|
||||||
if (cache && cache.expiry > Date.now()) {
|
if (cache && cache.expiry > Date.now()) {
|
||||||
return cache.data; // Return cached search results if not expired
|
return cache.searches[searchTerm] || null; // Return specific search term results
|
||||||
}
|
}
|
||||||
return null; // Cache expired or doesn't exist
|
return null; // Cache expired or doesn't exist
|
||||||
},
|
},
|
||||||
@ -100,10 +111,9 @@ export const useCacheStore = create<CacheState>((set, get) => ({
|
|||||||
clearExpiredCache: () =>
|
clearExpiredCache: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// Filter expired searches
|
|
||||||
const validSearchCache = Object.fromEntries(
|
const validSearchCache = Object.fromEntries(
|
||||||
Object.entries(state.searchCache).filter(
|
Object.entries(state.searchCache).filter(
|
||||||
([, value]) => value.expiry > now
|
([, value]) => value.expiry > now // Only keep unexpired lists
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
154
src/state/lists.ts
Normal file
154
src/state/lists.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import {create} from "zustand";
|
||||||
|
import { QortalMetadata } from "../types/interfaces/resources";
|
||||||
|
|
||||||
|
|
||||||
|
interface ListsState {
|
||||||
|
[listName: string]: {
|
||||||
|
name: string;
|
||||||
|
items: QortalMetadata[]; // ✅ Items are stored as an array
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListStore {
|
||||||
|
lists: ListsState;
|
||||||
|
|
||||||
|
// CRUD Operations
|
||||||
|
addList: (name: string, items: QortalMetadata[]) => void;
|
||||||
|
addItem: (listName: string, item: QortalMetadata) => void;
|
||||||
|
addItems: (listName: string, items: QortalMetadata[]) => void;
|
||||||
|
updateItem: (listName: string, item: QortalMetadata) => void;
|
||||||
|
deleteItem: (listName: string, itemKey: string) => void;
|
||||||
|
deleteList: (listName: string) => void;
|
||||||
|
|
||||||
|
// Getter function
|
||||||
|
getListByName: (listName: string) => QortalMetadata[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useListStore = create<ListStore>((set, get) => ({
|
||||||
|
lists: {},
|
||||||
|
|
||||||
|
addList: (name, items) =>
|
||||||
|
set((state) => ({
|
||||||
|
lists: {
|
||||||
|
...state.lists,
|
||||||
|
[name]: { name, items }, // ✅ Store items as an array
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
addItem: (listName, item) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.lists[listName]) return state; // Exit if list doesn't exist
|
||||||
|
|
||||||
|
const itemKey = `${item.name}-${item.service}-${item.identifier}`;
|
||||||
|
const existingItem = state.lists[listName].items.find(
|
||||||
|
(existing) => `${existing.name}-${existing.service}-${existing.identifier}` === itemKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingItem) return state; // Avoid duplicates
|
||||||
|
|
||||||
|
return {
|
||||||
|
lists: {
|
||||||
|
...state.lists,
|
||||||
|
[listName]: {
|
||||||
|
...state.lists[listName],
|
||||||
|
items: [...state.lists[listName].items, item], // ✅ Add to array
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
addItems: (listName, items) =>
|
||||||
|
set((state) => {
|
||||||
|
|
||||||
|
if (!state.lists[listName]) {
|
||||||
|
console.warn(`List "${listName}" does not exist. Creating a new list.`);
|
||||||
|
return {
|
||||||
|
lists: {
|
||||||
|
...state.lists,
|
||||||
|
[listName]: { name: listName, items: [...items] }, // ✅ Create new list if missing
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Generate existing keys correctly
|
||||||
|
const existingKeys = new Set(
|
||||||
|
state.lists[listName].items.map(
|
||||||
|
(item) => `${item.name}-${item.service}-${item.identifier}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// ✅ Ensure we correctly compare identifiers
|
||||||
|
const newItems = items.filter((item) => {
|
||||||
|
const itemKey = `${item.name}-${item.service}-${item.identifier}`;
|
||||||
|
const isDuplicate = existingKeys.has(itemKey);
|
||||||
|
|
||||||
|
return !isDuplicate; // ✅ Only keep items that are NOT in the existing list
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (newItems.length === 0) {
|
||||||
|
console.warn("No new items were added because they were all considered duplicates.");
|
||||||
|
return state; // ✅ Prevent unnecessary re-renders if no changes
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lists: {
|
||||||
|
...state.lists,
|
||||||
|
[listName]: {
|
||||||
|
...state.lists[listName],
|
||||||
|
items: [...state.lists[listName].items, ...newItems], // ✅ Append only new items
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateItem: (listName, item) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.lists[listName]) return state;
|
||||||
|
|
||||||
|
const itemKey = `${item.name}-${item.service}-${item.identifier}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
lists: {
|
||||||
|
...state.lists,
|
||||||
|
[listName]: {
|
||||||
|
...state.lists[listName],
|
||||||
|
items: state.lists[listName].items.map((existing) =>
|
||||||
|
`${existing.name}-${existing.service}-${existing.identifier}` === itemKey
|
||||||
|
? item // ✅ Update item
|
||||||
|
: existing
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteItem: (listName, itemKey) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.lists[listName]) return state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
lists: {
|
||||||
|
...state.lists,
|
||||||
|
[listName]: {
|
||||||
|
...state.lists[listName],
|
||||||
|
items: state.lists[listName].items.filter(
|
||||||
|
(item) => `${item.name}-${item.service}-${item.identifier}` !== itemKey
|
||||||
|
), // ✅ Remove from array
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteList: (listName) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.lists[listName]) return state;
|
||||||
|
|
||||||
|
const updatedLists = { ...state.lists };
|
||||||
|
delete updatedLists[listName];
|
||||||
|
|
||||||
|
return { lists: updatedLists };
|
||||||
|
}),
|
||||||
|
|
||||||
|
getListByName: (listName) => get().lists[listName]?.items || [], // ✅ Get a list by name
|
||||||
|
}));
|
Loading…
x
Reference in New Issue
Block a user