mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-14 17:41:20 +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
|
||||
});
|
||||
|
||||
const onSeenLastItemFunc = useCallback((lastItem: QortalMetadata) => {
|
||||
const onSeenLastItemFunc = (lastItem: QortalMetadata) => {
|
||||
if(onSeenLastItem){
|
||||
onSeenLastItem(lastItem)
|
||||
}
|
||||
|
||||
}, []);
|
||||
};
|
||||
|
||||
return (
|
||||
<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,
|
||||
} from "../../types/interfaces/resources";
|
||||
import { useResources } from "../../hooks/useResources";
|
||||
import { VirtualizedList } from "../../common/VirtualizedList";
|
||||
import { MessageWrapper, VirtualizedList } from "../../common/VirtualizedList";
|
||||
import { ListLoader } from "../../common/ListLoader";
|
||||
import { ListItem, useCacheStore } from "../../state/cache";
|
||||
import { ResourceLoader } from "./ResourceLoader";
|
||||
import { ItemCardWrapper } from "./ItemCardWrapper";
|
||||
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 {
|
||||
gap?: number;
|
||||
listLoadingHeight?: CSSProperties;
|
||||
disabledVirutalizationStyles?: {
|
||||
parentContainer?: CSSProperties;
|
||||
};
|
||||
}
|
||||
|
||||
interface DefaultLoaderParams {
|
||||
@ -29,17 +36,31 @@ interface DefaultLoaderParams {
|
||||
listItemErrorText?: string;
|
||||
}
|
||||
|
||||
interface PropsResourceListDisplay {
|
||||
interface BaseProps {
|
||||
params: QortalSearchParams;
|
||||
listItem: (item: ListItem, index: number) => React.ReactNode; // Function type
|
||||
listItem: (item: ListItem, index: number) => React.ReactNode;
|
||||
styles?: ResourceListStyles;
|
||||
loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode; // Function type
|
||||
loaderItem?: (status: "LOADING" | "ERROR") => React.ReactNode;
|
||||
defaultLoaderParams?: DefaultLoaderParams;
|
||||
loaderList?: (status: "LOADING" | "NO_RESULTS") => React.ReactNode; // Function type
|
||||
loaderList?: (status: "LOADING" | "NO_RESULTS") => React.ReactNode;
|
||||
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 = ({
|
||||
params,
|
||||
listItem,
|
||||
@ -50,19 +71,23 @@ export const ResourceListDisplay = ({
|
||||
loaderItem,
|
||||
loaderList,
|
||||
disableVirtualization,
|
||||
onSeenLastItem
|
||||
direction = "VERTICAL",
|
||||
onSeenLastItem,
|
||||
listName
|
||||
}: PropsResourceListDisplay) => {
|
||||
const [list, setList] = useState<QortalMetadata[]>([]);
|
||||
const { fetchResources } = useResources();
|
||||
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 getResourceList = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const parsedParams = JSON.parse(memoizedParams);
|
||||
const res = await fetchResources(parsedParams); // Awaiting the async function
|
||||
setList(res || []); // Ensure it's an array, avoid setting `undefined`
|
||||
const res = await fetchResources(parsedParams, listName, true); // Awaiting the async function
|
||||
addList(listName, res || [])
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch resources:", error);
|
||||
} finally {
|
||||
@ -70,10 +95,36 @@ export const ResourceListDisplay = ({
|
||||
}
|
||||
}, [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(() => {
|
||||
getResourceList();
|
||||
}, [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 (
|
||||
<ListLoader
|
||||
noResultsMessage={
|
||||
@ -96,10 +147,15 @@ export const ResourceListDisplay = ({
|
||||
>
|
||||
<div style={{ display: "flex", flexGrow: 1 }}>
|
||||
{!disableVirtualization && (
|
||||
<VirtualizedList list={list} onSeenLastItem={onSeenLastItem}>
|
||||
<VirtualizedList list={list} onSeenLastItem={(item)=> {
|
||||
getResourceMoreList()
|
||||
if(onSeenLastItem){
|
||||
onSeenLastItem(item)
|
||||
}
|
||||
}}>
|
||||
{(item: QortalMetadata, index: number) => (
|
||||
<>
|
||||
{styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}
|
||||
{styles?.gap && <Spacer height={`${styles.gap / 2}px`} />}
|
||||
<Spacer />
|
||||
<ListItemWrapper
|
||||
defaultLoaderParams={defaultLoaderParams}
|
||||
@ -108,27 +164,20 @@ export const ResourceListDisplay = ({
|
||||
render={listItem}
|
||||
renderListItemLoader={loaderItem}
|
||||
/>
|
||||
{styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}
|
||||
{styles?.gap && <Spacer height={`${styles.gap / 2}px`} />}
|
||||
</>
|
||||
)}
|
||||
</VirtualizedList>
|
||||
)}
|
||||
{disableVirtualization && (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{list?.map((item, index) => {
|
||||
{disableVirtualization && direction === "HORIZONTAL" && (
|
||||
<>
|
||||
<DynamicGrid
|
||||
gap={styles?.gap}
|
||||
items={list?.map((item, index) => {
|
||||
return (
|
||||
<React.Fragment
|
||||
key={`${item?.name}-${item?.service}-${item?.identifier}`}
|
||||
>
|
||||
{styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}
|
||||
<Spacer />
|
||||
<ListItemWrapper
|
||||
defaultLoaderParams={defaultLoaderParams}
|
||||
item={item}
|
||||
@ -136,10 +185,52 @@ export const ResourceListDisplay = ({
|
||||
render={listItem}
|
||||
renderListItemLoader={loaderItem}
|
||||
/>
|
||||
{styles?.gap && <Spacer height={`${styles.gap / 2}rem`} />}
|
||||
</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>
|
||||
|
@ -9,10 +9,45 @@ export const requestQueueProductPublishesBackup = new RequestQueueWithPromise(5)
|
||||
|
||||
export const useResources = () => {
|
||||
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(
|
||||
async (item: QortalMetadata) => {
|
||||
try {
|
||||
const key = `${item?.service}-${item?.name}-${item?.identifier}`;
|
||||
|
||||
const cachedProduct = getResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`);
|
||||
if (cachedProduct) return;
|
||||
setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, null);
|
||||
@ -20,17 +55,12 @@ export const useResources = () => {
|
||||
let res: string | undefined = undefined
|
||||
try {
|
||||
res = await requestQueueProductPublishes.enqueue((): Promise<string> => {
|
||||
return qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
identifier: item?.identifier,
|
||||
encoding: "base64",
|
||||
name: item?.name,
|
||||
service: item?.service,
|
||||
});
|
||||
return getArbitraryResource(`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, key)
|
||||
});
|
||||
} catch (error) {
|
||||
hasFailedToDownload = true
|
||||
}
|
||||
if(res === 'canceled') return false
|
||||
|
||||
if (hasFailedToDownload) {
|
||||
await new Promise((res) => {
|
||||
@ -41,13 +71,7 @@ export const useResources = () => {
|
||||
|
||||
try {
|
||||
res = await requestQueueProductPublishesBackup.enqueue((): Promise<string> => {
|
||||
return qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
identifier: item?.identifier,
|
||||
encoding: "base64",
|
||||
name: item?.name,
|
||||
service: item?.service,
|
||||
});
|
||||
return getArbitraryResource(`/arbitrary/${item?.service}/${item?.name}/${item?.identifier}?encoding=base64`, key)
|
||||
});
|
||||
} catch (error) {
|
||||
setResourceCache(`${item?.service}-${item?.name}-${item?.identifier}`, false);
|
||||
@ -79,9 +103,17 @@ export const useResources = () => {
|
||||
);
|
||||
|
||||
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 searchCache = getSearchCache(cacheKey);
|
||||
const searchCache = getSearchCache(listName, cacheKey);
|
||||
let responseData = [];
|
||||
|
||||
if (searchCache) {
|
||||
@ -96,7 +128,7 @@ export const useResources = () => {
|
||||
if (!response) throw new Error("Unable to fetch resources");
|
||||
responseData = response
|
||||
}
|
||||
setSearchCache(cacheKey, responseData);
|
||||
setSearchCache(listName, cacheKey, responseData);
|
||||
fetchDataFromResults(responseData);
|
||||
|
||||
return responseData;
|
||||
|
@ -3,13 +3,16 @@ import { QortalMetadata } from "../types/interfaces/resources";
|
||||
|
||||
|
||||
interface SearchCache {
|
||||
[searchTerm: string]: {
|
||||
data: QortalMetadata[]; // List of products for the search term
|
||||
expiry: number; // Expiry timestamp in milliseconds
|
||||
[listName: string]: {
|
||||
searches: {
|
||||
[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[]) => {
|
||||
const mergedArray = [...array1, ...array2];
|
||||
|
||||
@ -46,8 +49,8 @@ interface CacheState {
|
||||
// Search cache actions
|
||||
setResourceCache: (id: string, data: ListItem | false | null, customExpiry?: number) => void;
|
||||
|
||||
setSearchCache: (searchTerm: string, data: QortalMetadata[], customExpiry?: number) => void;
|
||||
getSearchCache: (searchTerm: string) => QortalMetadata[] | null;
|
||||
setSearchCache: (listName: string, searchTerm: string, data: QortalMetadata[], customExpiry?: number) => void;
|
||||
getSearchCache: (listName: string, searchTerm: string) => QortalMetadata[] | null;
|
||||
clearExpiredCache: () => void;
|
||||
getResourceCache: (id: string, ignoreExpire?: boolean) => ListItem | false | null;
|
||||
}
|
||||
@ -76,22 +79,30 @@ export const useCacheStore = create<CacheState>((set, get) => ({
|
||||
};
|
||||
}),
|
||||
// Add search results to cache
|
||||
setSearchCache: (searchTerm, data, customExpiry) =>
|
||||
setSearchCache: (listName, searchTerm, data, customExpiry) =>
|
||||
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 {
|
||||
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
|
||||
getSearchCache: (searchTerm) => {
|
||||
const cache = get().searchCache[searchTerm];
|
||||
getSearchCache: (listName, searchTerm) => {
|
||||
const cache = get().searchCache[listName];
|
||||
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
|
||||
},
|
||||
@ -100,10 +111,9 @@ export const useCacheStore = create<CacheState>((set, get) => ({
|
||||
clearExpiredCache: () =>
|
||||
set((state) => {
|
||||
const now = Date.now();
|
||||
// Filter expired searches
|
||||
const validSearchCache = Object.fromEntries(
|
||||
Object.entries(state.searchCache).filter(
|
||||
([, value]) => value.expiry > now
|
||||
([, value]) => value.expiry > now // Only keep unexpired lists
|
||||
)
|
||||
);
|
||||
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