This commit is contained in:
PhilReact 2025-03-14 02:03:23 +02:00
parent bb50cfc9a2
commit ae5111618f
5 changed files with 89 additions and 48 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "qapp-core", "name": "qapp-core",
"version": "1.0.0", "version": "1.0.2",
"description": "Qortal's core React library with global state, UI components, and utilities", "description": "Qortal's core React library with global state, UI components, and utilities",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.mjs", "module": "dist/index.mjs",

View File

@ -5,54 +5,33 @@ interface DynamicGridProps {
itemWidth?: number; // Minimum width per item itemWidth?: number; // Minimum width per item
gap?: number; // Spacing between grid items gap?: number; // Spacing between grid items
children: ReactNode children: ReactNode
minItemWidth?: number
} }
const DynamicGrid: React.FC<DynamicGridProps> = ({ const DynamicGrid: React.FC<DynamicGridProps> = ({
items, items,
itemWidth = 300, // Default min item width minItemWidth = 200, // Minimum width per item
gap = 10, // Default gap between items gap = 10, // Space between items
children 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 ( return (
<div style={{ display: "flex", flexDirection: 'column', alignItems: "center", width: "100%" }}> <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
{/* ✅ Centers the grid inside the parent */}
<div <div
ref={gridRef}
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: `repeat(${columns}, minmax(${itemWidth}px, 1fr))`, // ✅ Dynamically calculated columns gridTemplateColumns: `repeat(auto-fill, minmax(${minItemWidth}px, 1fr))`, // ✅ Expands to fit width
gap: `${gap}px`, gap: `${gap}px`,
width: "100%", // ✅ Ensures grid fills available space width: "100%",
maxWidth: "1200px", // ✅ Optional max width to prevent excessive stretching maxWidth: "100%", // ✅ Prevents overflow
margin: "auto", // ✅ Centers the grid horizontally margin: "auto",
gridAutoFlow: "row", // ✅ Ensures rows behave correctly overflow: "hidden", // ✅ Prevents horizontal scrollbars
gridAutoFlow: "row dense", // ✅ Fills space efficiently
}} }}
> >
{items.map((component, index) => ( {items.map((component, index) => (
<div key={index} style={{ width: "100%", display: "flex", justifyContent: "center" }}> <div key={index} style={{ width: "100%", display: "flex", justifyContent: "center", maxWidth: '400px' }}>
{component} {/* ✅ Render user-provided component */} {component} {/* ✅ Renders user-provided component */}
</div> </div>
))} ))}
</div> </div>
@ -61,4 +40,7 @@ const DynamicGrid: React.FC<DynamicGridProps> = ({
); );
}; };
export default DynamicGrid; export default DynamicGrid;

View File

@ -27,6 +27,9 @@ interface ResourceListStyles {
disabledVirutalizationStyles?: { disabledVirutalizationStyles?: {
parentContainer?: CSSProperties; parentContainer?: CSSProperties;
}; };
horizontalStyles?: {
minItemWidth?: number
}
} }
interface DefaultLoaderParams { interface DefaultLoaderParams {
@ -76,11 +79,15 @@ export const ResourceListDisplay = ({
listName listName
}: PropsResourceListDisplay) => { }: PropsResourceListDisplay) => {
const { fetchResources } = useResources(); const { fetchResources } = useResources();
const { getTemporaryResources } = useCacheStore();
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 addList = useListStore().addList
const addItems = useListStore().addItems const addItems = useListStore().addItems
const list = useListStore().getListByName(listName) const list = useListStore().getListByName(listName)
const listToDisplay = useMemo(()=> {
return [...getTemporaryResources(listName), ...list]
}, [list, listName])
const getResourceList = useCallback(async () => { const getResourceList = useCallback(async () => {
try { try {
@ -99,7 +106,8 @@ export const ResourceListDisplay = ({
try { try {
// setIsLoading(true); // setIsLoading(true);
const parsedParams = {...(JSON.parse(memoizedParams))}; 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 const res = await fetchResources(parsedParams, listName); // Awaiting the async function
addItems(listName, res || []) addItems(listName, res || [])
} catch (error) { } catch (error) {
@ -107,7 +115,7 @@ export const ResourceListDisplay = ({
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [memoizedParams, listName, list?.length]); }, [memoizedParams, listName, list]);
useEffect(() => { useEffect(() => {
getResourceList(); getResourceList();
@ -130,7 +138,7 @@ export const ResourceListDisplay = ({
noResultsMessage={ noResultsMessage={
defaultLoaderParams?.listNoResultsText || "No results available" defaultLoaderParams?.listNoResultsText || "No results available"
} }
resultsLength={list?.length} resultsLength={listToDisplay?.length}
isLoading={isLoading} isLoading={isLoading}
loadingMessage={ loadingMessage={
defaultLoaderParams?.listLoadingText || "Retrieving list. Please wait." defaultLoaderParams?.listLoadingText || "Retrieving list. Please wait."
@ -147,7 +155,7 @@ export const ResourceListDisplay = ({
> >
<div style={{ display: "flex", flexGrow: 1 }}> <div style={{ display: "flex", flexGrow: 1 }}>
{!disableVirtualization && ( {!disableVirtualization && (
<VirtualizedList list={list} onSeenLastItem={(item)=> { <VirtualizedList list={listToDisplay} onSeenLastItem={(item)=> {
getResourceMoreList() getResourceMoreList()
if(onSeenLastItem){ if(onSeenLastItem){
onSeenLastItem(item) onSeenLastItem(item)
@ -172,8 +180,9 @@ export const ResourceListDisplay = ({
{disableVirtualization && direction === "HORIZONTAL" && ( {disableVirtualization && direction === "HORIZONTAL" && (
<> <>
<DynamicGrid <DynamicGrid
minItemWidth={styles?.horizontalStyles?.minItemWidth}
gap={styles?.gap} gap={styles?.gap}
items={list?.map((item, index) => { items={listToDisplay?.map((item, index) => {
return ( return (
<React.Fragment <React.Fragment
key={`${item?.name}-${item?.service}-${item?.identifier}`} key={`${item?.name}-${item?.service}-${item?.identifier}`}
@ -190,12 +199,12 @@ export const ResourceListDisplay = ({
})} })}
> >
{!isLoading && list?.length > 0 && ( {!isLoading && listToDisplay?.length > 0 && (
<LazyLoad onLoadMore={()=> { <LazyLoad onLoadMore={()=> {
getResourceMoreList() getResourceMoreList()
if(onSeenLastItem){ if(onSeenLastItem){
onSeenLastItem(list[list?.length - 1]) onSeenLastItem(listToDisplay[listToDisplay?.length - 1])
} }
}} /> }} />
)} )}
@ -205,7 +214,7 @@ export const ResourceListDisplay = ({
)} )}
{disableVirtualization && direction === "VERTICAL" && ( {disableVirtualization && direction === "VERTICAL" && (
<div style={disabledVirutalizationStyles}> <div style={disabledVirutalizationStyles}>
{list?.map((item, index) => { {listToDisplay?.map((item, index) => {
return ( return (
<React.Fragment <React.Fragment
key={`${item?.name}-${item?.service}-${item?.identifier}`} key={`${item?.name}-${item?.service}-${item?.identifier}`}
@ -222,11 +231,11 @@ export const ResourceListDisplay = ({
</React.Fragment> </React.Fragment>
); );
})} })}
{!isLoading && list?.length > 0 && ( {!isLoading && listToDisplay?.length > 0 && (
<LazyLoad onLoadMore={()=> { <LazyLoad onLoadMore={()=> {
getResourceMoreList() getResourceMoreList()
if(onSeenLastItem){ if(onSeenLastItem){
onSeenLastItem(list[list?.length - 1]) onSeenLastItem(listToDisplay[listToDisplay?.length - 1])
} }
}} /> }} />
)} )}

View File

@ -7,8 +7,12 @@ import { base64ToUint8Array, uint8ArrayToObject } from '../utils/base64';
export const requestQueueProductPublishes = new RequestQueueWithPromise(20); export const requestQueueProductPublishes = new RequestQueueWithPromise(20);
export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(5); export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(5);
interface TemporaryResource {
qortalMetadata: QortalMetadata
data: any
}
export const useResources = () => { export const useResources = () => {
const { setSearchCache, getSearchCache, getResourceCache, setResourceCache } = useCacheStore(); const { setSearchCache, getSearchCache, getResourceCache, setResourceCache, addTemporaryResource, getTemporaryResources } = useCacheStore();
const requestControllers = new Map<string, AbortController>(); const requestControllers = new Map<string, AbortController>();
const getArbitraryResource = async (url: string, key: string): Promise<string> => { const getArbitraryResource = async (url: string, key: string): Promise<string> => {
@ -135,9 +139,16 @@ export const useResources = () => {
}, },
[getSearchCache, setSearchCache, fetchDataFromResults] [getSearchCache, setSearchCache, fetchDataFromResults]
); );
const addNewResources = useCallback((listName:string, temporaryResources: TemporaryResource[])=> {
addTemporaryResource(listName, temporaryResources.map((item)=> item.qortalMetadata))
}, [])
return { return {
fetchResources, fetchResources,
fetchIndividualPublish fetchIndividualPublish,
addNewResources
} }
} }

View File

@ -7,6 +7,7 @@ interface SearchCache {
searches: { searches: {
[searchTerm: string]: QortalMetadata[]; // List of products for each search term [searchTerm: string]: QortalMetadata[]; // List of products for each search term
}; };
temporaryNewResources: QortalMetadata[],
expiry: number; // Expiry timestamp for the whole list expiry: number; // Expiry timestamp for the whole list
}; };
} }
@ -53,6 +54,8 @@ interface CacheState {
getSearchCache: (listName: string, 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;
addTemporaryResource: (listName: string, newResources: QortalMetadata[], customExpiry?: number)=> void;
getTemporaryResources:(listName: string)=> QortalMetadata[]
} }
export const useCacheStore = create<CacheState>((set, get) => ({ export const useCacheStore = create<CacheState>((set, get) => ({
@ -91,6 +94,7 @@ export const useCacheStore = create<CacheState>((set, get) => ({
...(state.searchCache[listName]?.searches || {}), // Preserve existing searches ...(state.searchCache[listName]?.searches || {}), // Preserve existing searches
[searchTerm]: data, // Store new search term results [searchTerm]: data, // Store new search term results
}, },
temporaryNewResources: state.searchCache[listName]?.temporaryNewResources || [], // Preserve existing temp resources
expiry, // Expiry for the entire list expiry, // Expiry for the entire list
}, },
}, },
@ -106,7 +110,42 @@ export const useCacheStore = create<CacheState>((set, get) => ({
} }
return null; // Cache expired or doesn't exist 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<string, QortalMetadata>();
[...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 // Clear expired caches
clearExpiredCache: () => clearExpiredCache: () =>
set((state) => { set((state) => {