mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-15 01:41:21 +00:00
fix grid
This commit is contained in:
parent
bb50cfc9a2
commit
ae5111618f
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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])
|
||||||
}
|
}
|
||||||
}} />
|
}} />
|
||||||
)}
|
)}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user