Commit Graph

30 Commits

Author SHA1 Message Date
duckietm 59ed27b727 Revert "🆙 Bug fixed in localstorage"
This reverts commit 47453db5ee.
2026-06-04 13:43:29 +02:00
duckietm 47453db5ee 🆙 Bug fixed in localstorage 2026-06-04 13:43:04 +02:00
medievalshell f8e943d262 feat(catalog): live-merge imported furni on catalog open
On catalog open, re-fetch the custom furnidata chunk (custom/imported.json5) via
SessionDataManager.mergeFurnitureDataFromUrl() and feed the new entries to
RoomContentLoader.processFurnitureData(), so furniture imported from the admin
panel appears without a full client reload.
2026-06-03 23:28:33 +02:00
medievalshell dc0a8f965e feat(catalog): toggle stile catalogo classico/moderno
Aggiunge un checkbox nelle impostazioni utente per scegliere lo stile del
catalogo (classico vs moderno) + flag globale catalog.classic.style in
ui-config.json come default per tutti. Override per-utente in localStorage.
2026-05-29 23:36:06 +02:00
duckietm b3ff46a771 🆙 Fix Toolbar & Pets layout 2026-05-22 16:00:59 +02:00
duckietm 49917ed49b 🆕 Redesign of HC Club buy, now also give as gift 2026-05-21 14:00:03 +02:00
medievalshell d762f00c44 test(catalog): add getNodesByOfferId to useCatalogActions contract
`useCatalogActions` filter (useCatalog.ts:1042-1043) destructures and
returns `getNodesByOfferId` from the store along with the other action
methods. The filter contract test was stale — it asserted 11 keys while
the actual filter returns 12.

Add `getNodesByOfferId` to the expected keys list and to the fakeStore
mock so the assertion matches the live hook output.
2026-05-21 00:35:03 +02:00
duckietm 03795f975d 🆙 Fix Buy when search 2026-05-20 12:00:41 +02:00
simoleo89 779a98cae1 merge: sync upstream duckietm/Dev (b2318b9) into feat/react19-modernization
Absorbs 10 upstream commits (JSON5 config support, user-settings reset
password/email/username, wear-badge popup fix, login screen fix, About
update, offer selection logic, client path fix).

Conflicts resolved by keeping the modernized React 19 / Zustand / Form
Actions structure and porting upstream intent surgically:

- bootstrap.ts: kept GetConfiguration().init() pre-init + useEffectEvent,
  added JSON5 import (already wired into the parse fallback)
- LoginView.tsx: kept Form Actions (useActionState/useFormStatus); the
  upstream persistAccessTokenFromPayload(payload) fix was already
  integrated in the modernized SSO branch
- App.tsx: kept useEffectEvent import + StrictMode/ErrorBoundary umbrella
- vite.config.mjs: kept sirv plugin + react-compiler babel; absorbed
  upstream's base: process.env.VITE_BASE || './'
- package.json: kept superset (sirv, Vitest, Zustand, react-colorful,
  React Compiler) + added json5
- User-settings views: accepted upstream (duplicate of local cherry-pick
  2053c8e); notification badge bubble: accepted upstream fix

Verification: yarn typecheck clean, 193/193 Vitest, yarn build green.
2026-05-18 20:14:58 +02:00
simoleo89 8b4308af16 tests: co-locate every Vitest suite next to its subject under src/
Eliminate the parallel `tests/` tree. Each `*.test.ts` / `*.test.tsx`
now sits in the same directory as the module it covers, mirroring its
filename (`Foo.ts` ↔ `Foo.test.ts`). The renderer-SDK mock used by
component / hook tests moves to `src/__mocks__/nitro-renderer.ts` and
the Vitest setup file becomes `src/test-setup.ts` — both still wired
through `vitest.config.mts` exactly as before, only the paths changed.

All 13 suites + 178/178 cases still pass. The production build is
unaffected: rollup only follows imports from `src/index.tsx` and never
crosses into `.test.ts` files, so test code is naturally tree-shaken
out of the bundle. `yarn build` output is byte-for-byte the same on
the user-facing chunks.

tsconfig drops the now-redundant `tests` include entry. CLAUDE.md
'Layout convention' replaces the old `tests/` row with three rows
documenting the new co-located convention, the `__mocks__/` directory
and the `test-setup.ts` entry; ARCHITECTURE.md picks up the same
update. The 'DO NOT CHANGE' qualifier on the layout is preserved —
this rewrite IS the change, decided deliberately to make tests a
first-class part of the source tree rather than a sibling project.
2026-05-16 11:35:03 +02:00
Remco Epicnabbo 35385ffdd0 Simplify offer selection and activation logic
Always call applySelectedOffer(offer) and consolidate activation into a single conditional. Removed the separate non-lazy branch and now only call offer.activate() when offer.isLazy && offer.offerId > -1, reducing duplicated logic and simplifying the flow.
2026-05-15 13:55:56 +02:00
simoleo89 0f9fa1203b catalog: migrate remaining 36 useCatalog() consumers to the three filters
Replaces every direct call to the deprecated useCatalog() shim with the
targeted filter(s) (useCatalogData / useCatalogUiState / useCatalogActions).
Each consumer now subscribes only to the slice it actually reads, which
restores React Compiler memoization and stops catalog-wide re-renders
whenever an unrelated key changes.

Removes the now-unused useCatalog shim from useCatalog.ts and the
shim-specific case in tests/useCatalog.filters.test.tsx. The "all four
hooks observe the same singleton" test becomes "all three filters", since
there is no shim left to compare against. useCatalogFavorites swaps its
internal useCatalog() call for useCatalogUiState() (currentType lives in
the UI slice).

Updates CLAUDE.md and docs/ARCHITECTURE.md to reflect that all 48
historical consumers are migrated and the shim is gone.

Vitest: 162/162 (was 163 — minus the deprecated-shim contract case).
2026-05-14 20:05:44 +02:00
simoleo89 59d6c4cab3 catalog: three-way singleton-filter split + first 3 consumer migrations
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>
2026-05-13 21:50:56 +02:00
simoleo89 fd3ef7875d 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>
2026-05-13 21:42:04 +02:00
simoleo89 8b79233059 Extract useCatalogFavorites pure helpers + 16 Vitest cases
The 5 pure functions inside useCatalogFavorites
(normalizeCatalogType, getOffersStorageKey, getPagesStorageKey,
parseOffers, parsePages) handle the v2 -> v3 storage-key migration
that runs once per user the first time they open the v3 client. The
parseOffers branch in particular silently morphs the legacy number[]
shape into IFavoriteOffer[] — exactly the kind of one-shot migration
code that should have coverage so a refactor doesn't break old saves.

Move them into useCatalogFavorites.helpers.ts (sibling file, matching
the WiredCreatorTools / useInventoryFurni.reducers / avatarInfo.reducers
convention). useCatalogFavorites imports them back, plus re-exports
the IFavoriteOffer type from the helper module for the public API.

Both helpers import CatalogType from the concrete file path
('../../api/catalog/CatalogType') rather than the api barrel, so the
test file doesn't drag in the renderer SDK and run aground in jsdom.

Tests cover:
- normalizeCatalogType fallback to NORMAL on undefined/garbage/explicit
- storage-key routing for NORMAL / BUILDER / missing arg
- parseOffers: invalid JSON, non-array, empty array, v2 number[] migration,
  v3 IFavoriteOffer[] passthrough, mixed-array passthrough
- parsePages: invalid JSON, non-array, normal array

Net Vitest count: 83 -> 99 (7 test files).
2026-05-11 22:45:57 +02:00
simoleo89 7b062299de useClubGifts + useNitroEventInvalidator: close the catalogOptions bag
This commit drains the last field out of ICatalogOptions (clubGifts)
and deletes the interface — useCatalog no longer owns a catch-all
mutable object that downstream components stuff data into.

Two pieces:

1) New useNitroEventInvalidator(eventType, queryKey, accept?) — a
   small companion to useNitroQuery for the case where the server
   pushes the same event unprompted (e.g. ClubGiftInfoEvent fires
   both as the response to GetClubGiftInfo and again after the user
   claims a gift via SelectClubGiftComposer). It calls
   queryClient.invalidateQueries() on each matching push so the
   next render of any subscriber triggers a fresh queryFn.

2) New useClubGifts() — useNitroQuery on the ClubGiftInfoEvent
   pair, paired with useNitroEventInvalidator so server-driven
   pushes refresh the cache automatically. CatalogLayoutVipGiftsView
   now consumes the query directly. The local optimistic
   'giftsAvailable--' mutation (which side-effected the parser
   object passed back to the catalog state!) is dropped — the
   server's authoritative ClubGiftInfoEvent push is the single
   source of truth via the invalidator.

useCatalog drops the matching listener + the GetClubGiftInfo dispatch
from the catalog-open effect. ICatalogOptions is now empty and
deleted; the catalogOptions / setCatalogOptions state + return-shape
field are removed from useCatalog along with the import.
2026-05-11 22:38:32 +02:00
simoleo89 9a807bf335 useMarketplaceConfiguration: lift the marketplace config self-fetch
MarketplacePostOfferView was both *the* fetcher and the listener for
MarketplaceConfigurationEvent — it dispatched
GetMarketplaceConfigurationMessageComposer from one effect when item
was set, then routed the response through setCatalogOptions.

useCatalog never touched the field; it was passing through catalogOptions
purely as a transport mechanism for this single component to talk to
itself. Replace with useMarketplaceConfiguration() — staleTime Infinity
(server-side constants for a session), enabled on item, single tidy
data path.

Drops marketplaceConfiguration from ICatalogOptions; with petPalettes
out too, ICatalogOptions is now just { clubGifts }. clubGifts is the
last one and needs invalidation (server pushes ClubGiftInfoEvent after
SelectClubGiftComposer) so it stays put until useNitroEventInvalidator
companion lands.
2026-05-11 22:32:35 +02:00
simoleo89 3947781495 useSellablePetPalette(breed): per-breed TanStack query for pet picker
CatalogLayoutPetView previously read 'catalogOptions.petPalettes' (an
accumulating array of CatalogPetPalette objects keyed by breed) and,
on cache miss, dispatched GetSellablePetPalettesComposer(productData.type)
inline. useCatalog kept the matching SellablePetPalettesMessageEvent
listener that appended each new breed to the array (deduping by breed
identity).

Migrate the request/response pair to a TanStack query parameterized on
breed:

  useSellablePetPalette(breed)
    key:     ['nitro', 'catalog', 'petPalette', breed]
    request: () => new GetSellablePetPalettesComposer(breed)
    parser:  SellablePetPalettesMessageEvent
    accept:  event.getParser().productCode === breed
    select:  build a CatalogPetPalette from parser
    enabled: !!breed (avoid spamming composers before currentOffer is set)
    staleTime: Infinity

The view now derives breed from currentOffer.product.productData.type
and reads 'const { data: petPalette }'. The cache-miss-then-fetch
two-pass effect collapses into a single effect that runs once
petPalette resolves (or clears state when offer/petPalette aren't
ready).

Drops the matching listener from useCatalog, drops petPalettes from
ICatalogOptions, and removes the now-unused CatalogPetPalette /
SellablePetPalettesMessageEvent imports from useCatalog.
2026-05-11 22:28:55 +02:00
simoleo89 2a5b9a4a98 useClubOffers: per-windowId TanStack query for HC offer pages
Two catalog layouts each fire 'new GetClubOffersMessageComposer(windowId)'
on mount and read parser.offers via HabboClubOffersMessageEvent:

  - CatalogLayoutVipBuyView (windowId 1)
  - CatalogLayoutBuildersClubBuyView (windowId 2 / 3, depending on
    the addon variant)

Plus useCatalog used to also listen for HabboClubOffersMessageEvent and
stash the offers in 'catalogOptions.clubOffersByWindowId[windowId]' and
'catalogOptions.clubOffers' (the latter being a backward-compat alias
for windowId 1). Three listeners, three independent requests when all
mounted.

New useClubOffers(windowId) wraps the request/response pair as a
TanStack query keyed by '['nitro', 'catalog', 'clubOffers', windowId]'.
accept(): correlation-key filter (parser.windowId === windowId) so
the same multiplexed event doesn't satisfy the wrong query slot.

Both layouts now read 'const { data: offers = null } = useClubOffers(windowId)';
useCatalog drops the listener, ICatalogOptions drops the
clubOffers / clubOffersByWindowId fields and HabboClubOffersMessageEvent
no longer needs to be imported in useCatalog. The localization-refresh
effect that re-cloned both fields is also dropped — React Query owns
the cache now, and ClubOfferData has no localized strings anyway.
2026-05-11 22:23:19 +02:00
simoleo89 2d9785e931 useUserGroups: consolidate 4 dedup'd CatalogGroupsComposer call sites
Four independent components used to send 'new CatalogGroupsComposer()'
on mount and listen for GuildMembershipsMessageEvent:

  - useCatalog (writing into catalogOptions.groups)
  - CatalogLayoutGuildForumView
  - CatalogGuildSelectorWidgetView
  - WiredSelectorUsersGroupView
  - WiredConditionActorIsGroupMemberView

Each fired its own request and re-listened independently. With four
of them mounted in the wired-tools panel during a builder session,
the same packet went out four times.

New useUserGroups() hook wraps the request/response pair with
useNitroQuery (queryKey ['nitro', 'user', 'groups'], staleTime
Infinity — guild membership is session-stable). All four consumers
now read 'const { data: groups = [] } = useUserGroups()' and React
Query dedups: one composer at the first mount, all subsequent mounts
get the cached array.

Drops 'groups' from ICatalogOptions and the corresponding listener +
prev-state-merge from useCatalog — no remaining consumer reads it.
2026-05-11 22:14:39 +02:00
simoleo89 8e4544c5aa Migrate catalog giftConfiguration to useNitroQuery
The catalog's gift wrapping configuration was loaded by an effect in
useCatalog that fired GetGiftWrappingConfigurationComposer every time
the catalog opened, with the response stuffed into a catalogOptions
slice via setState-in-effect. Migrating to a TanStack query gives us
caching/dedup/loading-state for free on this one-shot session-stable
loader.

- New useGiftConfiguration() hook in src/hooks/catalog/ wraps the
  composer/parser pair with useNitroQuery and staleTime: Infinity
  (the wrapping config never changes within a session).
- CatalogGiftView now reads from the query directly instead of via
  catalogOptions; the useCatalog() call in that component is also
  dropped (no other field was used).
- useCatalog drops the GiftWrappingConfigurationEvent listener and the
  unconditional composer dispatch.
- ICatalogOptions loses the giftConfiguration? field — no remaining
  consumer.

First step toward the docs/ARCHITECTURE.md next-PR item 'Migrate
useCatalog read-only fetches to useNitroQuery'. The clubGifts loader
will follow once useNitroEventInvalidator lands (clubGifts can be
push-updated by the server after SelectClubGiftComposer, so it needs
cache invalidation, not just a one-shot fetch).
2026-05-11 21:12:34 +02:00
Lorenzune 726d1cc5c8 Merge latest duckie main with login UI 2026-04-21 11:53:30 +02:00
Lorenzune 9b36513def WIP preserve local changes before duckie merge 2026-04-21 11:13:32 +02:00
duckietm d3e6743fdf 🆙 Do not make have_offer static 2026-04-17 14:23:26 +02:00
duckietm 88117d937f 🆙 Fix Catalog editor 2026-04-17 13:51:46 +02:00
Lorenzune 954e477e47 feat: add builders club catalog ui flow 2026-04-07 14:40:51 +02:00
duckietm 33c31fe07d 🆕 Added New catalogue page 2026-03-23 15:02:20 +01:00
duckietm a0d0b5c4a4 Revert "Merge branch 'main' into furnisettingeditor-pr"
This reverts commit dfbfb1c2c1, reversing
changes made to 07702c44d0.
2026-03-23 11:49:05 +01:00
simoleo89 74dce1d55d feat(catalog): complete UI redesign with admin mode & favorites
- Modern card-based layout with vertical icon rail, breadcrumb nav, inline search
- Admin mode: edit/create/delete pages and offers, drag & drop reorder via HK API
- Favorites system: heart on furni, star on pages, localStorage persistence
- Redesigned product card with price pills, dynamic quantity spinner
- Upgraded trophies (filter tabs, parchment textarea), pets (breed/color flow),
  custom prefix (dynamic color boxes)
- Font fix: Ubuntu Regular, proper @font-face declarations
- New Tailwind design tokens and CatalogTexts.json for localization
2026-03-21 16:49:35 +01:00
DuckieTM 7feb10ab15 🆙 Init V3 2026-01-31 09:10:52 +01:00