mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
59d6c4cab3
Completes the useCatalog decomposition. After the previous commit
extracted the pure helpers, this one splits the singleton-via-useBetween
store into three slice-specific entry points and migrates a handful of
consumers as proof.
`src/hooks/catalog/useCatalog.ts`
- Internal `useCatalogState` → renamed to `useCatalogStore` and is no
longer exported. The full return shape is unchanged so callers that
still go through the shim see the exact same object.
- Three new exports built on top of the same `useBetween` instance:
- `useCatalogData()` — server-driven read-only slice (rootNode,
offersToNodes, currentPage, currentOffer, frontPageItems,
searchResult, roomPreviewer, isBusy, catalog localization
version, Builders Club counters + timers).
- `useCatalogUiState()` — UI ephemeral state + writers
(isVisible, pageId, previousPageId, currentType, activeNodes,
navigationHidden, purchaseOptions, catalogPlaceMultipleObjects,
plus every `set*` writer including the ones that mutate the
data slice on user-driven selection).
- `useCatalogActions()` — imperative operations only
(openCatalogByType, toggleCatalogByType, activateNode,
openPageBy{Id,Name,OfferId}, requestOfferToMover,
selectCatalogOffer, getNodeBy{Id,Name},
getBuilderFurniPlaceableStatus).
- `useCatalog` is kept as a deprecated shim that returns the full
historical surface, so the 48 existing consumers compile and run
unchanged.
Pilot consumer migrations (3 of 48):
- `CatalogBuildersClubStatusView` — Data (furni counters, seconds
timers) + UiState (currentType).
- `CatalogBreadcrumbView` — UiState (activeNodes) + Actions
(activateNode).
- `CatalogNavigationItemView` — UiState (currentType) + Actions
(activateNode).
Tests: `tests/useCatalog.filters.test.tsx` (5 cases).
`useBetween` is mocked via `vi.hoisted` so the four hooks share one
deterministic fake store — rendering the real `useCatalogStore`
would mount ~30 useState calls + open a fresh RoomPreviewer +
subscribe to a dozen renderer events, which is more than these
contract tests need.
- `useCatalogData` exposes exactly its read-only keys.
- `useCatalogUiState` exposes exactly its UI keys + setters.
- `useCatalogActions` exposes exactly its imperative ops (and
explicitly NOT data fields — proves no leak across slices).
- Singleton identity: callbacks read through the shim are `===` to
the ones read through the slices.
- Shim surface: the historical key set is still present so
un-migrated consumers don't silently break.
Suite: 163/163 (was 158/158). `yarn typecheck` green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
import { GetTickerTime } from '@nitrots/nitro-renderer';
|
|
import { FC, useEffect, useMemo, useState } from 'react';
|
|
import { CatalogType, FriendlyTime, LocalizeText } from '../../../../api';
|
|
import buildersClubIcon from '../../../../assets/images/toolbar/icons/buildersclub.png';
|
|
import { useCatalogData, useCatalogUiState } from '../../../../hooks';
|
|
|
|
export const CatalogBuildersClubStatusView: FC = () =>
|
|
{
|
|
const { furniCount = 0, furniLimit = 0, secondsLeft = 0, secondsLeftWithGrace = 0, updateTime = 0 } = useCatalogData();
|
|
const { currentType = CatalogType.NORMAL } = useCatalogUiState();
|
|
const [ ticker, setTicker ] = useState(() => GetTickerTime());
|
|
|
|
useEffect(() =>
|
|
{
|
|
if(currentType !== CatalogType.BUILDER) return;
|
|
|
|
const interval = window.setInterval(() => setTicker(GetTickerTime()), 1000);
|
|
|
|
return () => window.clearInterval(interval);
|
|
}, [ currentType ]);
|
|
|
|
const localizeOrDefault = (key: string, fallback: string, parameters: string[] = [], values: string[] = []) =>
|
|
{
|
|
const localized = LocalizeText(key, parameters, values);
|
|
|
|
return ((localized && (localized !== key)) ? localized : fallback);
|
|
};
|
|
|
|
const remainingSeconds = useMemo(() =>
|
|
{
|
|
const baseSeconds = (secondsLeft > 0) ? secondsLeft : secondsLeftWithGrace;
|
|
|
|
if(baseSeconds <= 0) return 0;
|
|
|
|
const elapsed = ((updateTime > 0) ? Math.floor((ticker - updateTime) / 1000) : 0);
|
|
|
|
return Math.max(0, (baseSeconds - elapsed));
|
|
}, [ secondsLeft, secondsLeftWithGrace, ticker, updateTime ]);
|
|
|
|
const isFullMember = (secondsLeft > 0);
|
|
const membershipStatus = localizeOrDefault(
|
|
isFullMember ? 'builder.header.status.member' : 'builder.header.status.trial',
|
|
isFullMember ? 'Membro Completo' : 'Prova Gratuita'
|
|
);
|
|
|
|
const title = localizeOrDefault(
|
|
'builder.header.title',
|
|
`Stato Builders' Club: ${ membershipStatus }`,
|
|
[ 'BCSTATUS' ],
|
|
[ membershipStatus ]
|
|
);
|
|
|
|
const durationText = localizeOrDefault(
|
|
'builder.header.status.membership',
|
|
`Tempo mancante: ${ FriendlyTime.format(remainingSeconds) }`,
|
|
[ 'DURATION' ],
|
|
[ FriendlyTime.format(remainingSeconds) ]
|
|
);
|
|
|
|
const limitText = localizeOrDefault(
|
|
'builder.header.status.limit',
|
|
`Furni usati: ${ furniCount }/${ furniLimit }`,
|
|
[ 'COUNT', 'LIMIT' ],
|
|
[ furniCount.toString(), furniLimit.toString() ]
|
|
);
|
|
|
|
if(currentType !== CatalogType.BUILDER) return null;
|
|
|
|
return (
|
|
<div className="builders-club-status-shell flex items-center gap-3 px-4 py-3">
|
|
<div className="builders-club-status-icon-shell flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-md">
|
|
<img alt="" className="h-[28px] w-[28px] object-contain" src={ buildersClubIcon } />
|
|
</div>
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate text-[13px] leading-none font-bold text-white">
|
|
{ title }
|
|
</span>
|
|
<span className="mt-1 text-[11px] leading-tight text-white/95">
|
|
{ durationText }
|
|
</span>
|
|
<span className="text-[11px] leading-tight text-[#ffba45]">
|
|
{ limitText }
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|