mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
catalog: extract pure helpers + 34 cases, consume them from useCatalog
First half of the proposed `useCatalog` decomposition. The 1036-line
god-hook still owns the singleton-via-useBetween, but the pure logic
it used to define inline now lives in a dependency-free module so it
can be tested in isolation and reused by future split-out hooks
(`useCatalogData` / `useCatalogUiState` / `useCatalogActions` when
those land).
New module: `src/hooks/catalog/useCatalog.helpers.ts` (222 LOC).
- `normalizeCatalogType(type?)` — coerce the optional catalog type to
`NORMAL` / `BUILDER`. Was a 5-line `useCallback` with an empty
dependency array.
- `getOfferProductKeys(offer)` — produces the canonical
`productType:id:classId` and `productType:class:className` keys
for the resolved-offer cache.
- `findNodeById` / `findNodeByName` — DFS over the catalog tree,
root explicitly excluded so callers can't select the synthetic
root by mistake.
- `getNodesByOfferIdFromMap(offerId, map, onlyVisible)` — extracted
from the closed-over `getNodesByOfferId`. The `onlyVisible`
fallback to the full bucket when nothing visible remains is
preserved.
- `buildCatalogNodeTree(NodeData)` — pulled out of the
`CatalogPagesListEvent` reducer. Builds the tree and the offerId
index in one pass; the caller now does `const { rootNode,
offersToNodes } = buildCatalogNodeTree(parser.root)` instead of
carrying an inline recursive walker + a local map.
- `resolveBuilderFurniPlaceableStatus(input)` — the placement
decision tree as a pure function. The hook keeps the
`GetRoomEngine` / `GetSessionDataManager` reads that count
non-self, non-moderator visitors (only when the subscription has
expired) and forwards the resulting `visitorCount` into the
helper, so the previous early-exit semantics are preserved.
`useCatalog.ts` now imports these and removes ~140 lines of inline
copies. Net hook size: 1036 → 961 LOC. Behavior unchanged.
Tests: `tests/useCatalog.helpers.test.ts` (34 cases).
- `normalizeCatalogType` (4) — BUILDER pass-through, NORMAL
pass-through, undefined/empty fallback, unknown string fallback.
- `getOfferProductKeys` (5) — both keys, id-only when classId<0,
class-only when className empty, no-product short-circuit,
empty productType short-circuit.
- `findNodeById` (5) — null input, root exclusion, immediate child,
grandchild, miss returns null.
- `findNodeByName` (2) — match by name + root exclusion, miss.
- `getNodesByOfferIdFromMap` (5) — empty map, raw bucket pass-through,
visible-only filter, fallback when no visible remain, miss.
- `buildCatalogNodeTree` (3) — root depth=0 + empty offer map for a
leaf-only root, DFS traversal tracks offer→nodes across branch
and leaf, child.parent === root.
- `resolveBuilderFurniPlaceableStatus` (10) — missing offer,
not-in-room, owner happy path, non-owner without fallback,
guild admin with time, furni limit reached, shared-pool override
ignoring the limit, expired+blocked-by-visitors flag,
expired+visitor count > 0, expired+empty room is okay.
To support the placement-status test the renderer mock gains real
numeric values for `RoomControllerLevel` (NONE..MODERATOR) and
`RoomObjectCategory` (MINIMUM..MAXIMUM); the previous string-keyed
Proxy stubs made `controllerLevel >= GUILD_ADMIN` evaluate to NaN.
Suite: 158/158 (was 124/124). `yarn typecheck` green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { NodeData, RoomControllerLevel, RoomObjectCategory, RoomObjectType } from '@nitrots/nitro-renderer';
|
||||
import { BuilderFurniPlaceableStatus, CatalogNode, CatalogType, ICatalogNode, IPurchasableOffer } from '../../api';
|
||||
|
||||
/**
|
||||
* Pure helpers extracted from `useCatalog.ts`. Each function takes the
|
||||
* relevant pieces of state as inputs (instead of closing over them via
|
||||
* useCallback) so it can be unit-tested without rendering a React tree.
|
||||
*
|
||||
* Keep these dependency-free at the React layer: no `useState`, no
|
||||
* refs, no `vi.fn`-able side effects beyond what the renderer SDK
|
||||
* already exposes (`GetRoomEngine`, etc., which the call sites guard).
|
||||
*/
|
||||
|
||||
/**
|
||||
* The catalog has two top-level "types" — the regular catalog and the
|
||||
* Builders Club catalog. Anything else maps to NORMAL. Centralising
|
||||
* the coercion in one place keeps the switch from drifting between
|
||||
* call sites and the message-event handlers.
|
||||
*/
|
||||
export const normalizeCatalogType = (type?: string): string =>
|
||||
{
|
||||
if(type === CatalogType.BUILDER) return CatalogType.BUILDER;
|
||||
|
||||
return CatalogType.NORMAL;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the canonical product-key list for a purchasable offer. Used
|
||||
* by the resolved-offer cache so the same offer can be looked up by
|
||||
* either `productType:id:classId` or `productType:class:className`.
|
||||
*/
|
||||
export const getOfferProductKeys = (offer: IPurchasableOffer | null | undefined): string[] =>
|
||||
{
|
||||
const keys: string[] = [];
|
||||
const product = offer?.product;
|
||||
|
||||
if(!product) return keys;
|
||||
|
||||
if(product.productType && (product.productClassId >= 0))
|
||||
{
|
||||
keys.push(`${ product.productType }:id:${ product.productClassId }`);
|
||||
}
|
||||
|
||||
if(product.productType && product.furnitureData?.className?.length)
|
||||
{
|
||||
keys.push(`${ product.productType }:class:${ product.furnitureData.className }`);
|
||||
}
|
||||
|
||||
return keys;
|
||||
};
|
||||
|
||||
/**
|
||||
* Depth-first search by pageId. The root is excluded so callers never
|
||||
* select the synthetic "root" node by mistake. Recursive but bounded
|
||||
* by the tree the server sends (typically 3-4 levels deep).
|
||||
*/
|
||||
export const findNodeById = (id: number, node: ICatalogNode | null, rootNode: ICatalogNode | null): ICatalogNode | null =>
|
||||
{
|
||||
if(!node) return null;
|
||||
if((node.pageId === id) && (node !== rootNode)) return node;
|
||||
|
||||
for(const child of node.children)
|
||||
{
|
||||
const found = findNodeById(id, child, rootNode);
|
||||
|
||||
if(found) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Depth-first search by pageName. Same exclusion of the root as
|
||||
* `findNodeById`.
|
||||
*/
|
||||
export const findNodeByName = (name: string, node: ICatalogNode | null, rootNode: ICatalogNode | null): ICatalogNode | null =>
|
||||
{
|
||||
if(!node) return null;
|
||||
if((node.pageName === name) && (node !== rootNode)) return node;
|
||||
|
||||
for(const child of node.children)
|
||||
{
|
||||
const found = findNodeByName(name, child, rootNode);
|
||||
|
||||
if(found) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lookup the list of catalog nodes a given offer appears under. When
|
||||
* `onlyVisible` is true the helper falls back to the full list if the
|
||||
* filtered subset is empty — matches the original behavior in
|
||||
* `getNodesByOfferId(offerId, true)`.
|
||||
*/
|
||||
export const getNodesByOfferIdFromMap = (
|
||||
offerId: number,
|
||||
offersToNodes: Map<number, ICatalogNode[]> | null | undefined,
|
||||
onlyVisible: boolean = false
|
||||
): ICatalogNode[] | null =>
|
||||
{
|
||||
if(!offersToNodes || !offersToNodes.size) return null;
|
||||
|
||||
if(onlyVisible)
|
||||
{
|
||||
const offers = offersToNodes.get(offerId);
|
||||
const visible: ICatalogNode[] = [];
|
||||
|
||||
if(offers && offers.length)
|
||||
{
|
||||
for(const offer of offers)
|
||||
{
|
||||
if(offer.isVisible) visible.push(offer);
|
||||
}
|
||||
}
|
||||
|
||||
if(visible.length) return visible;
|
||||
}
|
||||
|
||||
return offersToNodes.get(offerId) ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn the server-side NodeData tree into a CatalogNode tree paired
|
||||
* with an offerId → nodes index map. Pure (besides the `new
|
||||
* CatalogNode` construction).
|
||||
*
|
||||
* Original lived inline inside the `CatalogPagesListEvent` handler;
|
||||
* extracted so the reducer is testable without rendering the hook.
|
||||
*/
|
||||
export const buildCatalogNodeTree = (root: NodeData): { rootNode: ICatalogNode; offersToNodes: Map<number, ICatalogNode[]> } =>
|
||||
{
|
||||
const offersToNodes: Map<number, ICatalogNode[]> = new Map();
|
||||
|
||||
const walk = (node: NodeData, depth: number, parent: ICatalogNode | null): ICatalogNode =>
|
||||
{
|
||||
const catalogNode = (new CatalogNode(node, depth, parent) as ICatalogNode);
|
||||
|
||||
for(const offerId of catalogNode.offerIds)
|
||||
{
|
||||
const existing = offersToNodes.get(offerId);
|
||||
|
||||
if(existing) existing.push(catalogNode);
|
||||
else offersToNodes.set(offerId, [ catalogNode ]);
|
||||
}
|
||||
|
||||
for(const child of node.children) catalogNode.addChild(walk(child, depth + 1, catalogNode));
|
||||
|
||||
return catalogNode;
|
||||
};
|
||||
|
||||
return { rootNode: walk(root, 0, null), offersToNodes };
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure-input version of the placement-status decision. The original
|
||||
* `getBuilderFurniPlaceableStatus` closes over a handful of state
|
||||
* slices + reads `GetRoomSession()` / `GetRoomEngine()` /
|
||||
* `GetSessionDataManager()` directly. Pulling those reads up to the
|
||||
* call site (the hook still does them) makes the rest of the
|
||||
* decision tree testable in isolation.
|
||||
*
|
||||
* `roomSession` may be null (user is in the hotel view, not a room).
|
||||
* `usersInRoomMinusSelf` is the number of non-moderator, non-self
|
||||
* users sharing the room — only consulted when `secondsLeft <= 0`,
|
||||
* because the limit-reached / not-in-room paths short-circuit first.
|
||||
*/
|
||||
export interface BuilderPlacementStatusInput
|
||||
{
|
||||
offer: IPurchasableOffer | null | undefined;
|
||||
roomSession: { isGuildRoom: boolean; isRoomOwner: boolean; controllerLevel: number } | null;
|
||||
secondsLeft: number;
|
||||
furniCount: number;
|
||||
furniLimit: number;
|
||||
builderPlacementAllowedInCurrentRoom: boolean;
|
||||
builderPlacementBlockedByVisitors: boolean;
|
||||
/** Count of non-moderator, non-self users in the room. Only consulted when `secondsLeft <= 0`. */
|
||||
visitorCount?: number;
|
||||
}
|
||||
|
||||
export const resolveBuilderFurniPlaceableStatus = (input: BuilderPlacementStatusInput): BuilderFurniPlaceableStatus =>
|
||||
{
|
||||
const { offer, roomSession, secondsLeft, furniCount, furniLimit, builderPlacementAllowedInCurrentRoom, builderPlacementBlockedByVisitors, visitorCount = 0 } = input;
|
||||
|
||||
if(!offer) return BuilderFurniPlaceableStatus.MISSING_OFFER;
|
||||
|
||||
if(!roomSession) return BuilderFurniPlaceableStatus.NOT_IN_ROOM;
|
||||
|
||||
const canUseGuildAdminFallback = (roomSession.isGuildRoom
|
||||
&& (roomSession.controllerLevel >= RoomControllerLevel.GUILD_ADMIN)
|
||||
&& (secondsLeft > 0));
|
||||
|
||||
const usesSharedPlacementPool = (!roomSession.isRoomOwner && (builderPlacementAllowedInCurrentRoom || canUseGuildAdminFallback));
|
||||
|
||||
if(!roomSession.isRoomOwner && !builderPlacementAllowedInCurrentRoom && !canUseGuildAdminFallback)
|
||||
{
|
||||
return BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN;
|
||||
}
|
||||
|
||||
if(!usesSharedPlacementPool && ((furniCount < 0) || (furniCount >= furniLimit)))
|
||||
{
|
||||
return BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED;
|
||||
}
|
||||
|
||||
if((secondsLeft <= 0) && builderPlacementBlockedByVisitors)
|
||||
{
|
||||
return BuilderFurniPlaceableStatus.VISITORS_IN_ROOM;
|
||||
}
|
||||
|
||||
if((secondsLeft <= 0) && (visitorCount > 0))
|
||||
{
|
||||
return BuilderFurniPlaceableStatus.VISITORS_IN_ROOM;
|
||||
}
|
||||
|
||||
return BuilderFurniPlaceableStatus.OKAY;
|
||||
};
|
||||
|
||||
// Re-exports for the legacy categories so the call site in useCatalog
|
||||
// doesn't need to know whether the constant comes from the renderer
|
||||
// SDK enum or our own copy.
|
||||
export { RoomControllerLevel, RoomObjectCategory, RoomObjectType };
|
||||
+48
-123
@@ -1,10 +1,11 @@
|
||||
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { BuilderFurniPlaceableStatus, CatalogNode, CatalogPage, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api';
|
||||
import { BuilderFurniPlaceableStatus, CatalogPage, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api';
|
||||
import { CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, CatalogPurchasedEvent, InventoryFurniAddedEvent } from '../../events';
|
||||
import { useMessageEvent, useNitroEvent, useUiEvent } from '../events';
|
||||
import { useNotification } from '../notification';
|
||||
import { buildCatalogNodeTree, findNodeById, findNodeByName, getNodesByOfferIdFromMap, getOfferProductKeys, normalizeCatalogType, resolveBuilderFurniPlaceableStatus, RoomControllerLevel, RoomObjectCategory, RoomObjectType } from './useCatalog.helpers';
|
||||
import { useCatalogPlaceMultipleItems } from './useCatalogPlaceMultipleItems';
|
||||
import { useCatalogSkipPurchaseConfirmation } from './useCatalogSkipPurchaseConfirmation';
|
||||
|
||||
@@ -62,13 +63,6 @@ const useCatalogState = () =>
|
||||
setIsVisible(false);
|
||||
}, []);
|
||||
|
||||
const normalizeCatalogType = useCallback((type?: string) =>
|
||||
{
|
||||
if(type === CatalogType.BUILDER) return CatalogType.BUILDER;
|
||||
|
||||
return CatalogType.NORMAL;
|
||||
}, []);
|
||||
|
||||
const resetVisibleCatalogState = useCallback((type?: string) =>
|
||||
{
|
||||
requestedPage.current.resetRequest();
|
||||
@@ -85,7 +79,7 @@ const useCatalogState = () =>
|
||||
setFrontPageItems([]);
|
||||
setNavigationHidden(false);
|
||||
setCurrentType(normalizeCatalogType(type));
|
||||
}, [ normalizeCatalogType ]);
|
||||
}, []);
|
||||
|
||||
const openCatalogByType = useCallback((type?: string) =>
|
||||
{
|
||||
@@ -97,7 +91,7 @@ const useCatalogState = () =>
|
||||
}
|
||||
|
||||
setIsVisible(true);
|
||||
}, [ currentType, normalizeCatalogType, resetVisibleCatalogState ]);
|
||||
}, [ currentType, resetVisibleCatalogState ]);
|
||||
|
||||
const toggleCatalogByType = useCallback((type?: string) =>
|
||||
{
|
||||
@@ -116,54 +110,60 @@ const useCatalogState = () =>
|
||||
}
|
||||
|
||||
setIsVisible(true);
|
||||
}, [ isVisible, currentType, normalizeCatalogType, resetVisibleCatalogState ]);
|
||||
}, [ isVisible, currentType, resetVisibleCatalogState ]);
|
||||
|
||||
const getBuilderFurniPlaceableStatus = useCallback((offer: IPurchasableOffer) =>
|
||||
{
|
||||
if(!offer) return BuilderFurniPlaceableStatus.MISSING_OFFER;
|
||||
|
||||
const roomSession = GetRoomSession();
|
||||
const canUseGuildAdminFallback = (!!roomSession
|
||||
&& roomSession.isGuildRoom
|
||||
&& (roomSession.controllerLevel >= RoomControllerLevel.GUILD_ADMIN)
|
||||
&& (secondsLeft > 0));
|
||||
const usesSharedPlacementPool = (!!roomSession && !roomSession.isRoomOwner && (builderPlacementAllowedInCurrentRoom || canUseGuildAdminFallback));
|
||||
|
||||
if(!roomSession) return BuilderFurniPlaceableStatus.NOT_IN_ROOM;
|
||||
// Count non-self, non-moderator users sharing the room. Only
|
||||
// matters when the subscription has expired — the pure helper
|
||||
// short-circuits on the limit-reached / not-in-room paths
|
||||
// first, so we skip the room scan when there's still time on
|
||||
// the clock.
|
||||
let visitorCount = 0;
|
||||
|
||||
if(!roomSession.isRoomOwner && !builderPlacementAllowedInCurrentRoom && !canUseGuildAdminFallback) return BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN;
|
||||
|
||||
if(!usesSharedPlacementPool && ((furniCount < 0) || (furniCount >= furniLimit))) return BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED;
|
||||
|
||||
if((secondsLeft <= 0) && builderPlacementBlockedByVisitors) return BuilderFurniPlaceableStatus.VISITORS_IN_ROOM;
|
||||
|
||||
if(secondsLeft <= 0)
|
||||
if(roomSession && (secondsLeft <= 0) && !builderPlacementBlockedByVisitors)
|
||||
{
|
||||
const roomEngine = GetRoomEngine();
|
||||
const userDataManager = roomSession.userDataManager;
|
||||
const sessionDataManager = GetSessionDataManager();
|
||||
|
||||
if(!roomEngine || !userDataManager || !sessionDataManager) return BuilderFurniPlaceableStatus.OKAY;
|
||||
|
||||
const roomObjects = roomEngine.getRoomObjects(roomSession.roomId, RoomObjectCategory.UNIT);
|
||||
|
||||
if(!roomObjects || !roomObjects.length) return BuilderFurniPlaceableStatus.OKAY;
|
||||
|
||||
for(const roomObject of roomObjects)
|
||||
if(roomEngine && userDataManager && sessionDataManager)
|
||||
{
|
||||
if(!roomObject) continue;
|
||||
const roomObjects = roomEngine.getRoomObjects(roomSession.roomId, RoomObjectCategory.UNIT);
|
||||
|
||||
const userData = userDataManager.getUserDataByIndex(roomObject.id);
|
||||
if(roomObjects && roomObjects.length)
|
||||
{
|
||||
for(const roomObject of roomObjects)
|
||||
{
|
||||
if(!roomObject) continue;
|
||||
|
||||
if(!userData || (userData.type !== RoomObjectType.USER)) continue;
|
||||
if(userData.webID === sessionDataManager.userId) continue;
|
||||
if(userData.isModerator) continue;
|
||||
const userData = userDataManager.getUserDataByIndex(roomObject.id);
|
||||
|
||||
return BuilderFurniPlaceableStatus.VISITORS_IN_ROOM;
|
||||
if(!userData || (userData.type !== RoomObjectType.USER)) continue;
|
||||
if(userData.webID === sessionDataManager.userId) continue;
|
||||
if(userData.isModerator) continue;
|
||||
|
||||
visitorCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BuilderFurniPlaceableStatus.OKAY;
|
||||
return resolveBuilderFurniPlaceableStatus({
|
||||
offer,
|
||||
roomSession: roomSession
|
||||
? { isGuildRoom: roomSession.isGuildRoom, isRoomOwner: roomSession.isRoomOwner, controllerLevel: roomSession.controllerLevel }
|
||||
: null,
|
||||
secondsLeft,
|
||||
furniCount,
|
||||
furniLimit,
|
||||
builderPlacementAllowedInCurrentRoom,
|
||||
builderPlacementBlockedByVisitors,
|
||||
visitorCount
|
||||
});
|
||||
}, [ builderPlacementAllowedInCurrentRoom, builderPlacementBlockedByVisitors, furniCount, furniLimit, secondsLeft ]);
|
||||
|
||||
const isDraggable = useCallback((offer: IPurchasableOffer) =>
|
||||
@@ -294,70 +294,12 @@ const useCatalogState = () =>
|
||||
});
|
||||
}, [ resetObjectMover, resetRoomPaint ]);
|
||||
|
||||
const getNodeById = useCallback((id: number, node: ICatalogNode) =>
|
||||
{
|
||||
if((node.pageId === id) && (node !== rootNode)) return node;
|
||||
const getNodeById = useCallback((id: number, node: ICatalogNode) => findNodeById(id, node, rootNode), [ rootNode ]);
|
||||
|
||||
for(const child of node.children)
|
||||
{
|
||||
const found = (getNodeById(id, child));
|
||||
|
||||
if(found) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [ rootNode ]);
|
||||
|
||||
const getNodeByName = useCallback((name: string, node: ICatalogNode) =>
|
||||
{
|
||||
if((node.pageName === name) && (node !== rootNode)) return node;
|
||||
|
||||
for(const child of node.children)
|
||||
{
|
||||
const found = (getNodeByName(name, child));
|
||||
|
||||
if(found) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [ rootNode ]);
|
||||
const getNodeByName = useCallback((name: string, node: ICatalogNode) => findNodeByName(name, node, rootNode), [ rootNode ]);
|
||||
|
||||
const getNodesByOfferId = useCallback((offerId: number, flag: boolean = false) =>
|
||||
{
|
||||
if(!offersToNodes || !offersToNodes.size) return null;
|
||||
|
||||
if(flag)
|
||||
{
|
||||
const nodes: ICatalogNode[] = [];
|
||||
const offers = offersToNodes.get(offerId);
|
||||
|
||||
if(offers && offers.length) for(const offer of offers) (offer.isVisible && nodes.push(offer));
|
||||
|
||||
if(nodes.length) return nodes;
|
||||
}
|
||||
|
||||
return offersToNodes.get(offerId);
|
||||
}, [ offersToNodes ]);
|
||||
|
||||
const getOfferProductKeys = useCallback((offer: IPurchasableOffer) =>
|
||||
{
|
||||
const product = offer?.product;
|
||||
const keys: string[] = [];
|
||||
|
||||
if(!product) return keys;
|
||||
|
||||
if(product.productType && (product.productClassId >= 0))
|
||||
{
|
||||
keys.push(`${ product.productType }:id:${ product.productClassId }`);
|
||||
}
|
||||
|
||||
if(product.productType && product.furnitureData?.className?.length)
|
||||
{
|
||||
keys.push(`${ product.productType }:class:${ product.furnitureData.className }`);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}, []);
|
||||
getNodesByOfferIdFromMap(offerId, offersToNodes, flag), [ offersToNodes ]);
|
||||
|
||||
const cacheResolvedOffer = useCallback((offer: IPurchasableOffer) =>
|
||||
{
|
||||
@@ -365,7 +307,7 @@ const useCatalogState = () =>
|
||||
{
|
||||
resolvedOffersByProductKey.current.set(key, offer);
|
||||
}
|
||||
}, [ getOfferProductKeys ]);
|
||||
}, []);
|
||||
|
||||
const applySelectedOffer = useCallback((offer: IPurchasableOffer) =>
|
||||
{
|
||||
@@ -563,27 +505,10 @@ const useCatalogState = () =>
|
||||
|
||||
if(parserCatalogType !== currentType) return;
|
||||
|
||||
const offers: Map<number, ICatalogNode[]> = new Map();
|
||||
const { rootNode: builtRoot, offersToNodes: builtOffers } = buildCatalogNodeTree(parser.root);
|
||||
|
||||
const getCatalogNode = (node: NodeData, depth: number, parent: ICatalogNode) =>
|
||||
{
|
||||
const catalogNode = (new CatalogNode(node, depth, parent) as ICatalogNode);
|
||||
|
||||
for(const offerId of catalogNode.offerIds)
|
||||
{
|
||||
if(offers.has(offerId)) offers.get(offerId).push(catalogNode);
|
||||
else offers.set(offerId, [ catalogNode ]);
|
||||
}
|
||||
|
||||
depth++;
|
||||
|
||||
for(const child of node.children) catalogNode.addChild(getCatalogNode(child, depth, catalogNode));
|
||||
|
||||
return catalogNode;
|
||||
};
|
||||
|
||||
setRootNode(getCatalogNode(parser.root, 0, null));
|
||||
setOffersToNodes(offers);
|
||||
setRootNode(builtRoot);
|
||||
setOffersToNodes(builtOffers);
|
||||
});
|
||||
|
||||
useMessageEvent<CatalogPageMessageEvent>(CatalogPageMessageEvent, event =>
|
||||
|
||||
Reference in New Issue
Block a user