diff --git a/package.json b/package.json index 24d10c2..e40ea7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qapp-core", - "version": "1.0.0", + "version": "1.0.2", "description": "Qortal's core React library with global state, UI components, and utilities", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/src/components/ResourceList/DynamicGrid.tsx b/src/components/ResourceList/DynamicGrid.tsx index dc2d506..026715e 100644 --- a/src/components/ResourceList/DynamicGrid.tsx +++ b/src/components/ResourceList/DynamicGrid.tsx @@ -5,54 +5,33 @@ interface DynamicGridProps { itemWidth?: number; // Minimum width per item gap?: number; // Spacing between grid items children: ReactNode + minItemWidth?: number } const DynamicGrid: React.FC = ({ items, - itemWidth = 300, // Default min item width - gap = 10, // Default gap between items - children + minItemWidth = 200, // Minimum width per item + gap = 10, // Space between items + children, }) => { - const gridRef = useRef(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 ( -
- {/* ✅ Centers the grid inside the parent */} +
{items.map((component, index) => ( -
- {component} {/* ✅ Render user-provided component */} +
+ {component} {/* ✅ Renders user-provided component */}
))}
@@ -61,4 +40,7 @@ const DynamicGrid: React.FC = ({ ); }; + + + export default DynamicGrid; diff --git a/src/components/ResourceList/ResourceListDisplay.tsx b/src/components/ResourceList/ResourceListDisplay.tsx index b80aca7..6126fff 100644 --- a/src/components/ResourceList/ResourceListDisplay.tsx +++ b/src/components/ResourceList/ResourceListDisplay.tsx @@ -27,6 +27,9 @@ interface ResourceListStyles { disabledVirutalizationStyles?: { parentContainer?: CSSProperties; }; + horizontalStyles?: { + minItemWidth?: number + } } interface DefaultLoaderParams { @@ -76,11 +79,15 @@ export const ResourceListDisplay = ({ listName }: PropsResourceListDisplay) => { const { fetchResources } = useResources(); + const { getTemporaryResources } = useCacheStore(); const [isLoading, setIsLoading] = useState(false); const memoizedParams = useMemo(() => JSON.stringify(params), [params]); const addList = useListStore().addList const addItems = useListStore().addItems const list = useListStore().getListByName(listName) + const listToDisplay = useMemo(()=> { + return [...getTemporaryResources(listName), ...list] + }, [list, listName]) const getResourceList = useCallback(async () => { try { @@ -99,7 +106,8 @@ export const ResourceListDisplay = ({ try { // setIsLoading(true); const parsedParams = {...(JSON.parse(memoizedParams))}; - parsedParams.offset = (parsedParams?.offset || 0) + list.length + parsedParams.before = list.length === 0 ? null : list[list.length - 1]?.created + parsedParams.offset = null const res = await fetchResources(parsedParams, listName); // Awaiting the async function addItems(listName, res || []) } catch (error) { @@ -107,7 +115,7 @@ export const ResourceListDisplay = ({ } finally { setIsLoading(false); } - }, [memoizedParams, listName, list?.length]); + }, [memoizedParams, listName, list]); useEffect(() => { getResourceList(); @@ -130,7 +138,7 @@ export const ResourceListDisplay = ({ noResultsMessage={ defaultLoaderParams?.listNoResultsText || "No results available" } - resultsLength={list?.length} + resultsLength={listToDisplay?.length} isLoading={isLoading} loadingMessage={ defaultLoaderParams?.listLoadingText || "Retrieving list. Please wait." @@ -147,7 +155,7 @@ export const ResourceListDisplay = ({ >
{!disableVirtualization && ( - { + { getResourceMoreList() if(onSeenLastItem){ onSeenLastItem(item) @@ -172,8 +180,9 @@ export const ResourceListDisplay = ({ {disableVirtualization && direction === "HORIZONTAL" && ( <> { + items={listToDisplay?.map((item, index) => { return ( - {!isLoading && list?.length > 0 && ( + {!isLoading && listToDisplay?.length > 0 && ( { getResourceMoreList() if(onSeenLastItem){ - onSeenLastItem(list[list?.length - 1]) + onSeenLastItem(listToDisplay[listToDisplay?.length - 1]) } }} /> )} @@ -205,7 +214,7 @@ export const ResourceListDisplay = ({ )} {disableVirtualization && direction === "VERTICAL" && (
- {list?.map((item, index) => { + {listToDisplay?.map((item, index) => { return ( ); })} - {!isLoading && list?.length > 0 && ( + {!isLoading && listToDisplay?.length > 0 && ( { getResourceMoreList() if(onSeenLastItem){ - onSeenLastItem(list[list?.length - 1]) + onSeenLastItem(listToDisplay[listToDisplay?.length - 1]) } }} /> )} diff --git a/src/hooks/useResources.tsx b/src/hooks/useResources.tsx index 8003e75..d62e24e 100644 --- a/src/hooks/useResources.tsx +++ b/src/hooks/useResources.tsx @@ -7,8 +7,12 @@ import { base64ToUint8Array, uint8ArrayToObject } from '../utils/base64'; export const requestQueueProductPublishes = new RequestQueueWithPromise(20); export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(5); +interface TemporaryResource { + qortalMetadata: QortalMetadata + data: any +} export const useResources = () => { - const { setSearchCache, getSearchCache, getResourceCache, setResourceCache } = useCacheStore(); + const { setSearchCache, getSearchCache, getResourceCache, setResourceCache, addTemporaryResource, getTemporaryResources } = useCacheStore(); const requestControllers = new Map(); const getArbitraryResource = async (url: string, key: string): Promise => { @@ -135,9 +139,16 @@ export const useResources = () => { }, [getSearchCache, setSearchCache, fetchDataFromResults] ); + + + + const addNewResources = useCallback((listName:string, temporaryResources: TemporaryResource[])=> { + addTemporaryResource(listName, temporaryResources.map((item)=> item.qortalMetadata)) + }, []) return { fetchResources, - fetchIndividualPublish + fetchIndividualPublish, + addNewResources } } diff --git a/src/state/cache.ts b/src/state/cache.ts index 8e9a06b..da9ee0f 100644 --- a/src/state/cache.ts +++ b/src/state/cache.ts @@ -7,6 +7,7 @@ interface SearchCache { searches: { [searchTerm: string]: QortalMetadata[]; // List of products for each search term }; + temporaryNewResources: QortalMetadata[], expiry: number; // Expiry timestamp for the whole list }; } @@ -53,6 +54,8 @@ interface CacheState { getSearchCache: (listName: string, searchTerm: string) => QortalMetadata[] | null; clearExpiredCache: () => void; getResourceCache: (id: string, ignoreExpire?: boolean) => ListItem | false | null; + addTemporaryResource: (listName: string, newResources: QortalMetadata[], customExpiry?: number)=> void; + getTemporaryResources:(listName: string)=> QortalMetadata[] } export const useCacheStore = create((set, get) => ({ @@ -91,6 +94,7 @@ export const useCacheStore = create((set, get) => ({ ...(state.searchCache[listName]?.searches || {}), // Preserve existing searches [searchTerm]: data, // Store new search term results }, + temporaryNewResources: state.searchCache[listName]?.temporaryNewResources || [], // Preserve existing temp resources expiry, // Expiry for the entire list }, }, @@ -106,7 +110,42 @@ export const useCacheStore = create((set, get) => ({ } return null; // Cache expired or doesn't exist }, - + addTemporaryResource: (listName, newResources, customExpiry) => + set((state) => { + const expiry = Date.now() + (customExpiry || 5 * 60 * 1000); // Reset expiry + + const existingResources = state.searchCache[listName]?.temporaryNewResources || []; + + // Merge and remove duplicates, keeping the latest by `created` timestamp + const uniqueResourcesMap = new Map(); + + [...existingResources, ...newResources].forEach((item) => { + const key = `${item.service}-${item.name}-${item.identifier}`; + const existingItem = uniqueResourcesMap.get(key); + + if (!existingItem || item.created > existingItem.created) { + uniqueResourcesMap.set(key, item); + } + }); + + return { + searchCache: { + ...state.searchCache, + [listName]: { + ...state.searchCache[listName], + temporaryNewResources: Array.from(uniqueResourcesMap.values()), // Store unique items + expiry, // Reset expiry + }, + }, + }; + }), + getTemporaryResources: (listName: string) => { + const cache = get().searchCache[listName]; + if (cache && cache.expiry > Date.now()) { + return cache.temporaryNewResources || []; + } + return []; // Return empty array if expired or doesn't exist + }, // Clear expired caches clearExpiredCache: () => set((state) => {