- { /* Admin: quick actions */ }
- { adminMode && !catalogAdmin.editingPageData &&
-
-
-
-
}
-
- { /* Top card: preview + name + purchase */ }
-
- { /* Pet preview */ }
-
-
-
- { ((petIndex > -1) && (petIndex <= 7)) &&
- }
-
-
- { /* Pet info */ }
-
-
-
-
- { petBreedName || LocalizeText('catalog.pet.breed') }
- { adminMode && currentOffer &&
- catalogAdmin.setEditingOffer(currentOffer) }
- /> }
-
- { adminMode && currentOffer &&
-
- ID: { currentOffer.product.productClassId }
- Offer: { currentOffer.offerId }
-
}
- { !!page.localization.getText(0) &&
-
}
-
-
- { /* Name input */ }
-
-
-
- 0 ? 'border-danger bg-danger/5' : approvalResult === 0 ? 'border-success bg-success/5' : 'border-card-grid-item-border focus:border-primary bg-white' }` }
- placeholder={ LocalizeText('widgets.petpackage.name.title') }
- type="text"
- value={ petName }
- onChange={ event => setPetName(event.target.value) }
- />
- { approvalResult === 0 &&
- }
- { approvalResult > 0 &&
- }
-
- { (approvalResult > 0) &&
-
{ validationErrorMessage } }
-
-
- { /* Price + buy */ }
-
-
-
-
-
-
-
- { /* Breed/Color grid */ }
-
-
-
- { colorsShowing ? LocalizeText('catalog.pets.choose.color') : LocalizeText('catalog.pets.choose.breed') }
-
- { colorsShowing &&
- }
-
-
- { !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) => (
-
setSelectedPaletteIndex(index) }
- >
-
-
- )) }
- { colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => (
-
setSelectedColorIndex(index) }
- />
- )) }
-
-
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/layout/vip-gifts/CatalogLayoutVipGiftsView.tsx b/src/components/catalog-modern/views/page/layout/vip-gifts/CatalogLayoutVipGiftsView.tsx
deleted file mode 100644
index 2614b13..0000000
--- a/src/components/catalog-modern/views/page/layout/vip-gifts/CatalogLayoutVipGiftsView.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { SelectClubGiftComposer } from '@nitrots/nitro-renderer';
-import { FC, useCallback, useMemo } from 'react';
-import { LocalizeText, SendMessageComposer } from '../../../../../../api';
-import { AutoGrid, Text } from '../../../../../../common';
-import { useClubGifts, useNotification, usePurse } from '../../../../../../hooks';
-import { CatalogLayoutProps } from '../CatalogLayout.types';
-import { VipGiftItem } from './VipGiftItemView';
-
-let isSelectingGift = false;
-
-export const CatalogLayoutVipGiftsView: FC
= props =>
-{
- const { purse = null } = usePurse();
- const { data: clubGifts = null } = useClubGifts();
- const { showConfirm = null } = useNotification();
-
- const giftsAvailable = useCallback(() =>
- {
- if(!clubGifts) return '';
-
- if(clubGifts.giftsAvailable > 0) return LocalizeText('catalog.club_gift.available', [ 'amount' ], [ clubGifts.giftsAvailable.toString() ]);
-
- if(clubGifts.daysUntilNextGift > 0) return LocalizeText('catalog.club_gift.days_until_next', [ 'days' ], [ clubGifts.daysUntilNextGift.toString() ]);
-
- if(purse.isVip) return LocalizeText('catalog.club_gift.not_available');
-
- return LocalizeText('catalog.club_gift.no_club');
- }, [ clubGifts, purse ]);
-
- const selectGift = useCallback((localizationId: string) =>
- {
- showConfirm(LocalizeText('catalog.club_gift.confirm'), () =>
- {
- if(isSelectingGift) return;
-
- isSelectingGift = true;
-
- // The server replies with a fresh ClubGiftInfoEvent after
- // accepting the selection; useClubGifts subscribes to that
- // event via useNitroEventInvalidator, so giftsAvailable
- // refreshes from the authoritative source — no need to
- // mutate the parser locally.
- SendMessageComposer(new SelectClubGiftComposer(localizationId));
-
- setTimeout(() => isSelectingGift = false, 5000);
- }, null);
- }, [ showConfirm ]);
-
- const sortGifts = useMemo(() =>
- {
- if(!clubGifts) return [];
-
- return [ ...clubGifts.offers ].sort((a, b) =>
- (clubGifts.getOfferExtraData(a.offerId).daysRequired - clubGifts.getOfferExtraData(b.offerId).daysRequired)
- );
- }, [ clubGifts ]);
-
-
- return (
- <>
- { giftsAvailable() }
-
- { clubGifts && (clubGifts.offers.length > 0) && sortGifts.map(offer => 0)) } offer={ offer } onSelect={ selectGift }/>) }
-
- >
- );
-};
diff --git a/src/components/catalog-modern/views/page/layout/vip-gifts/VipGiftItemView.tsx b/src/components/catalog-modern/views/page/layout/vip-gifts/VipGiftItemView.tsx
deleted file mode 100644
index 4ca0c41..0000000
--- a/src/components/catalog-modern/views/page/layout/vip-gifts/VipGiftItemView.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { CatalogPageMessageOfferData } from '@nitrots/nitro-renderer';
-import { FC, useCallback } from 'react';
-import { LocalizeText, ProductImageUtility } from '../../../../../../api';
-import { Button, LayoutGridItem, LayoutImage, Text } from '../../../../../../common';
-
-export interface VipGiftItemViewProps
-{
- offer: CatalogPageMessageOfferData;
- isAvailable: boolean;
- daysRequired: number;
- onSelect(localizationId: string): void;
-}
-
-export const VipGiftItem : FC = props =>
-{
- const { offer = null, isAvailable = false, daysRequired = 0, onSelect = null } = props;
-
- const getImageUrlForOffer = useCallback( () =>
- {
- if(!offer || !offer.products.length) return '';
-
- const productData = offer.products[0];
-
- return ProductImageUtility.getProductImageUrl(productData.productType, productData.furniClassId, productData.extraParam);
- }, [ offer ]);
-
- const getItemTitle = useCallback(() =>
- {
- if(!offer || !offer.products.length) return '';
-
- const productData = offer.products[0];
-
- const localizationKey = ProductImageUtility.getProductCategory(productData.productType, productData.furniClassId) === 2 ? 'wallItem.name.' + productData.furniClassId : 'roomItem.name.' + productData.furniClassId;
-
- return LocalizeText(localizationKey);
- }, [ offer ]);
-
- const getItemDesc = useCallback( () =>
- {
- if(!offer || !offer.products.length) return '';
-
- const productData = offer.products[0];
-
- const localizationKey = ProductImageUtility.getProductCategory(productData.productType, productData.furniClassId) === 2 ? 'wallItem.desc.' + productData.furniClassId : 'roomItem.desc.' + productData.furniClassId ;
-
- return LocalizeText(localizationKey);
- }, [ offer ]);
-
- const getMonthsRequired = useCallback(() =>
- {
- return Math.floor(daysRequired / 31);
- },[ daysRequired ]);
-
- return (
-
-
- { getItemTitle() }
-
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogAddOnBadgeWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogAddOnBadgeWidgetView.tsx
deleted file mode 100644
index 657729b..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogAddOnBadgeWidgetView.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { FC } from 'react';
-import { BaseProps, LayoutBadgeImageView } from '../../../../../common';
-import { useCatalogData } from '../../../../../hooks';
-
-interface CatalogAddOnBadgeWidgetViewProps extends BaseProps
-{
-
-}
-
-export const CatalogAddOnBadgeWidgetView: FC = props =>
-{
- const { ...rest } = props;
- const { currentOffer = null } = useCatalogData();
-
- if(!currentOffer || !currentOffer.badgeCode || !currentOffer.badgeCode.length) return null;
-
- return ;
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogBadgeSelectorWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogBadgeSelectorWidgetView.tsx
deleted file mode 100644
index 8ef5c4d..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogBadgeSelectorWidgetView.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { StringDataType } from '@nitrots/nitro-renderer';
-import { FC, useEffect, useMemo, useState } from 'react';
-import { AutoGrid, AutoGridProps, LayoutBadgeImageView, LayoutGridItem } from '../../../../../common';
-import { useCatalogData, useCatalogUiState, useInventoryBadges } from '../../../../../hooks';
-
-const EXCLUDED_BADGE_CODES: string[] = [];
-
-interface CatalogBadgeSelectorWidgetViewProps extends AutoGridProps
-{
-
-}
-
-export const CatalogBadgeSelectorWidgetView: FC = props =>
-{
- const { columnCount = 5, ...rest } = props;
- const [ isVisible, setIsVisible ] = useState(false);
- const [ currentBadgeCode, setCurrentBadgeCode ] = useState(null);
- const { currentOffer = null } = useCatalogData();
- const { setPurchaseOptions = null } = useCatalogUiState();
- const { badgeCodes = [], activate = null, deactivate = null } = useInventoryBadges();
-
- const previewStuffData = useMemo(() =>
- {
- if(!currentBadgeCode) return null;
-
- const stuffData = new StringDataType();
-
- stuffData.setValue([ '0', currentBadgeCode, '', '' ]);
-
- return stuffData;
- }, [ currentBadgeCode ]);
-
- useEffect(() =>
- {
- if(!currentOffer) return;
-
- setPurchaseOptions(prevValue =>
- {
- const newValue = { ...prevValue };
-
- newValue.extraParamRequired = true;
- newValue.extraData = ((previewStuffData && previewStuffData.getValue(1)) || null);
- newValue.previewStuffData = previewStuffData;
-
- return newValue;
- });
- }, [ currentOffer, previewStuffData, setPurchaseOptions ]);
-
- useEffect(() =>
- {
- if(!isVisible) return;
-
- const id = activate();
-
- return () => deactivate(id);
- }, [ isVisible, activate, deactivate ]);
-
- useEffect(() =>
- {
- setIsVisible(true);
-
- return () => setIsVisible(false);
- }, []);
-
- return (
-
- { badgeCodes && (badgeCodes.length > 0) && badgeCodes.map((badgeCode, index) =>
- {
- return (
- setCurrentBadgeCode(badgeCode) }>
-
-
- );
- }) }
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogBundleGridWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogBundleGridWidgetView.tsx
deleted file mode 100644
index 249bc87..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogBundleGridWidgetView.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { FC, useEffect, useRef } from 'react';
-import { AutoGrid, AutoGridProps, LayoutGridItem } from '../../../../../common';
-import { useCatalogData } from '../../../../../hooks';
-import { getFurniIconUrl } from '../common/getFurniIconUrl';
-
-interface CatalogBundleGridWidgetViewProps extends AutoGridProps
-{
-
-}
-
-export const CatalogBundleGridWidgetView: FC = props =>
-{
- const { columnCount = 5, children = null, ...rest } = props;
- const { currentOffer = null } = useCatalogData();
- const elementRef = useRef(null);
-
- useEffect(() =>
- {
- if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
- }, [ currentOffer ]);
-
- if(!currentOffer) return null;
-
- return (
-
- { currentOffer.products && (currentOffer.products.length > 0) && currentOffer.products.map((product, index) => ) }
- { children }
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogFirstProductSelectorWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogFirstProductSelectorWidgetView.tsx
deleted file mode 100644
index c5e9542..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogFirstProductSelectorWidgetView.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { FC, useEffect } from 'react';
-import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
-
-export const CatalogFirstProductSelectorWidgetView: FC<{}> = props =>
-{
- const { currentPage = null } = useCatalogData();
- const { setCurrentOffer = null } = useCatalogUiState();
-
- useEffect(() =>
- {
- if(!currentPage || !currentPage.offers.length) return;
-
- setCurrentOffer(currentPage.offers[0]);
- }, [ currentPage, setCurrentOffer ]);
-
- return null;
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogGuildBadgeWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogGuildBadgeWidgetView.tsx
deleted file mode 100644
index c69c895..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogGuildBadgeWidgetView.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { StringDataType } from '@nitrots/nitro-renderer';
-import { FC, useMemo } from 'react';
-import { BaseProps, LayoutBadgeImageView } from '../../../../../common';
-import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
-
-interface CatalogGuildBadgeWidgetViewProps extends BaseProps
-{
-
-}
-
-export const CatalogGuildBadgeWidgetView: FC = props =>
-{
- const { ...rest } = props;
- const { currentOffer = null } = useCatalogData();
- const { purchaseOptions = null } = useCatalogUiState();
- const { previewStuffData = null } = purchaseOptions;
-
- const badgeCode = useMemo(() =>
- {
- if(!currentOffer || !previewStuffData) return null;
-
- const badgeCode = (previewStuffData as StringDataType).getValue(2);
-
- if(!badgeCode || !badgeCode.length) return null;
-
- return badgeCode;
- }, [ currentOffer, previewStuffData ]);
-
- if(!badgeCode) return null;
-
- return ;
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogGuildSelectorWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogGuildSelectorWidgetView.tsx
deleted file mode 100644
index 3d9e4a4..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogGuildSelectorWidgetView.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { StringDataType } from '@nitrots/nitro-renderer';
-import { FC, useEffect, useMemo, useState } from 'react';
-import { LocalizeText } from '../../../../../api';
-import { Button, Flex } from '../../../../../common';
-import { useCatalogData, useCatalogUiState, useUserGroups } from '../../../../../hooks';
-
-export const CatalogGuildSelectorWidgetView: FC<{}> = props =>
-{
- const [ selectedGroupIndex, setSelectedGroupIndex ] = useState(0);
- const { currentOffer = null } = useCatalogData();
- const { setPurchaseOptions = null } = useCatalogUiState();
- const { data: groups = null } = useUserGroups();
-
- const previewStuffData = useMemo(() =>
- {
- if(!groups || !groups.length) return null;
-
- const group = groups[selectedGroupIndex];
-
- if(!group) return null;
-
- const stuffData = new StringDataType();
-
- stuffData.setValue([ '0', group.groupId.toString(), group.badgeCode, group.colorA, group.colorB ]);
-
- return stuffData;
- }, [ selectedGroupIndex, groups ]);
-
- useEffect(() =>
- {
- if(!currentOffer) return;
-
- setPurchaseOptions(prevValue =>
- {
- const newValue = { ...prevValue };
-
- newValue.extraParamRequired = true;
- newValue.extraData = ((previewStuffData && previewStuffData.getValue(1)) || null);
- newValue.previewStuffData = previewStuffData;
-
- return newValue;
- });
- }, [ currentOffer, previewStuffData, setPurchaseOptions ]);
-
- if(!groups || !groups.length)
- {
- return (
-
- { LocalizeText('catalog.guild_selector.members_only') }
-
-
- );
- }
-
- const selectedGroup = groups[selectedGroupIndex];
-
- return (
-
- { !!selectedGroup &&
-
-
-
- }
-
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogItemGridWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogItemGridWidgetView.tsx
deleted file mode 100644
index e34dbb8..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogItemGridWidgetView.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { FC, useCallback, useEffect, useRef, useState } from 'react';
-import { IPurchasableOffer } from '../../../../../api';
-import { AutoGrid, AutoGridProps } from '../../../../../common';
-import { useCatalogActions, useCatalogData } from '../../../../../hooks';
-import { useCatalogAdmin } from '../../../CatalogAdminContext';
-import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
-
-interface CatalogItemGridWidgetViewProps extends AutoGridProps
-{
-
-}
-
-export const CatalogItemGridWidgetView: FC = props =>
-{
- const { columnCount = 5, children = null, ...rest } = props;
- const { currentOffer = null, currentPage = null } = useCatalogData();
- const { selectCatalogOffer = null } = useCatalogActions();
- const catalogAdmin = useCatalogAdmin();
- const adminMode = catalogAdmin?.adminMode ?? false;
- const elementRef = useRef(null);
- const [ dragIndex, setDragIndex ] = useState(null);
- const [ dropIndex, setDropIndex ] = useState(null);
-
- useEffect(() =>
- {
- if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
- }, [ currentPage ]);
-
- // Drag-and-drop handlers — hooks MUST run unconditionally so the
- // hook order stays stable when currentPage flips from null to a
- // real value (the `if(!currentPage) return null` below would
- // otherwise hide these from the first render and React would flag
- // "Rendered more hooks than during the previous render"). Bodies
- // are safe to evaluate pre-load: currentPage? optional chaining
- // already guards the only access inside handleDrop.
- const handleDragStart = useCallback((index: number) =>
- {
- setDragIndex(index);
- }, []);
-
- const handleDragOver = useCallback((e: React.DragEvent, index: number) =>
- {
- e.preventDefault();
- setDropIndex(index);
- }, []);
-
- const handleDrop = useCallback((index: number) =>
- {
- if(dragIndex !== null && dragIndex !== index && currentPage?.offers)
- {
- const offers = [ ...currentPage.offers ];
- const [ moved ] = offers.splice(dragIndex, 1);
-
- offers.splice(index, 0, moved);
-
- const orders = offers.map((o, i) => ({ id: o.offerId, orderNumber: i }));
-
- catalogAdmin?.reorderOffers(orders);
- }
-
- setDragIndex(null);
- setDropIndex(null);
- }, [ dragIndex, currentPage, catalogAdmin ]);
-
- const handleDragEnd = useCallback(() =>
- {
- setDragIndex(null);
- setDropIndex(null);
- }, []);
-
- if(!currentPage) return null;
-
- const selectOffer = (offer: IPurchasableOffer) =>
- {
- selectCatalogOffer(offer);
- };
-
- return (
-
- { currentPage.offers && (currentPage.offers.length > 0) && currentPage.offers.map((offer, index) =>
- {
- const isDragging = dragIndex === index;
- const isDropTarget = dropIndex === index && dragIndex !== index;
-
- return (
- handleDragOver(e, index) : undefined }
- onDragStart={ adminMode ? () => handleDragStart(index) : undefined }
- onDrop={ adminMode ? () => handleDrop(index) : undefined }
- >
-
-
- );
- }) }
- { children }
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogLimitedItemWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogLimitedItemWidgetView.tsx
deleted file mode 100644
index faf02a2..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogLimitedItemWidgetView.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { FC } from 'react';
-import { Offer } from '../../../../../api';
-import { LayoutLimitedEditionCompletePlateView } from '../../../../../common';
-import { useCatalogData } from '../../../../../hooks';
-
-export const CatalogLimitedItemWidgetView: FC = props =>
-{
- const { currentOffer = null } = useCatalogData();
-
- if(!currentOffer || (currentOffer.pricingModel !== Offer.PRICING_MODEL_SINGLE) || !currentOffer.product.isUniqueLimitedItem) return null;
-
- return (
-
-
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogPriceDisplayWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogPriceDisplayWidgetView.tsx
deleted file mode 100644
index a83a981..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogPriceDisplayWidgetView.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { FC } from 'react';
-import { FaPlus } from 'react-icons/fa';
-import { IPurchasableOffer } from '../../../../../api';
-import { LayoutCurrencyIcon, Text } from '../../../../../common';
-import { useCatalogUiState } from '../../../../../hooks';
-
-interface CatalogPriceDisplayWidgetViewProps
-{
- offer: IPurchasableOffer;
- separator?: boolean;
-}
-
-export const CatalogPriceDisplayWidgetView: FC = props =>
-{
- const { offer = null, separator = false } = props;
- const { purchaseOptions = null } = useCatalogUiState();
- const { quantity = 1 } = purchaseOptions;
-
- if(!offer) return null;
-
- return (
-
- { (offer.priceInCredits > 0) &&
-
- { (offer.priceInCredits * quantity) }
-
-
}
- { separator && (offer.priceInCredits > 0) && (offer.priceInActivityPoints > 0) &&
-
}
- { (offer.priceInActivityPoints > 0) &&
-
- { (offer.priceInActivityPoints * quantity) }
-
-
}
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogPurchaseWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogPurchaseWidgetView.tsx
deleted file mode 100644
index 473f469..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogPurchaseWidgetView.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-import { CreateLinkEvent, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
-import { FC, useCallback, useEffect, useMemo, useState } from 'react';
-import { BuilderFurniPlaceableStatus, CatalogPurchaseState, CatalogType, DispatchUiEvent, GetClubMemberLevel, LocalizeText, NotificationBubbleType, Offer, ProductTypeEnum, SendMessageComposer } from '../../../../../api';
-import { Button, LayoutLoadingSpinnerView, Text } from '../../../../../common';
-import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, CatalogPurchasedEvent } from '../../../../../events';
-import { useCatalogActions, useCatalogData, useCatalogSkipPurchaseConfirmation, useCatalogUiState, useNotification, usePurse, useUiEvent } from '../../../../../hooks';
-
-interface CatalogPurchaseWidgetViewProps
-{
- noGiftOption?: boolean;
- purchaseCallback?: () => void;
-}
-
-let isPurchasingCatalogItem = false;
-
-export const CatalogPurchaseWidgetView: FC = props =>
-{
- const { noGiftOption = false, purchaseCallback = null } = props;
- const [ builderPlaceableRefreshTick, setBuilderPlaceableRefreshTick ] = useState(0);
- const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
- const [ catalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation();
- const { currentOffer = null, currentPage = null } = useCatalogData();
- const { currentType = CatalogType.NORMAL, purchaseOptions = null, setPurchaseOptions = null, setCatalogPlaceMultipleObjects = null } = useCatalogUiState();
- const { requestOfferToMover = null, getBuilderFurniPlaceableStatus = null, getNodesByOfferId = null } = useCatalogActions();
- const { getCurrencyAmount = null } = usePurse();
- const { showSingleBubble = null } = useNotification();
-
- const onCatalogEvent = useCallback((event: CatalogEvent) =>
- {
- switch(event.type)
- {
- case CatalogPurchasedEvent.PURCHASE_SUCCESS:
- isPurchasingCatalogItem = false;
- setPurchaseState(CatalogPurchaseState.NONE);
- return;
- case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
- isPurchasingCatalogItem = false;
- setPurchaseState(CatalogPurchaseState.FAILED);
- return;
- case CatalogPurchaseNotAllowedEvent.NOT_ALLOWED:
- isPurchasingCatalogItem = false;
- setPurchaseState(CatalogPurchaseState.FAILED);
- return;
- case CatalogPurchaseSoldOutEvent.SOLD_OUT:
- isPurchasingCatalogItem = false;
- setPurchaseState(CatalogPurchaseState.SOLD_OUT);
- return;
- }
- }, []);
-
- useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
- useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
- useUiEvent(CatalogPurchaseNotAllowedEvent.NOT_ALLOWED, onCatalogEvent);
- useUiEvent(CatalogPurchaseSoldOutEvent.SOLD_OUT, onCatalogEvent);
-
- const isLimitedSoldOut = useMemo(() =>
- {
- if(!currentOffer) return false;
-
- if(purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)) return false;
-
- if(currentOffer.pricingModel === Offer.PRICING_MODEL_SINGLE)
- {
- const product = currentOffer.product;
-
- if(product && product.isUniqueLimitedItem) return !product.uniqueLimitedItemsLeft;
- }
-
- return false;
- }, [ currentOffer, purchaseOptions ]);
-
- const purchase = (isGift: boolean = false) =>
- {
- if(!currentOffer || isPurchasingCatalogItem) return;
-
- if(GetClubMemberLevel() < currentOffer.clubLevel)
- {
- CreateLinkEvent('habboUI/open/hccenter');
-
- return;
- }
-
- if(isGift)
- {
- DispatchUiEvent(new CatalogInitGiftEvent(currentOffer.page.pageId, currentOffer.offerId, purchaseOptions.extraData));
-
- return;
- }
-
- isPurchasingCatalogItem = true;
- setPurchaseState(CatalogPurchaseState.PURCHASE);
-
- setTimeout(() =>
- {
- isPurchasingCatalogItem = false;
- }, 10000);
-
- if(purchaseCallback)
- {
- purchaseCallback();
-
- return;
- }
-
- let pageId = currentOffer.page.pageId;
-
- if(pageId === -1 && getNodesByOfferId)
- {
- const nodes = getNodesByOfferId(currentOffer.offerId);
- if(nodes && nodes.length) pageId = nodes[0].pageId;
- }
-
- SendMessageComposer(new PurchaseFromCatalogComposer(pageId, currentOffer.offerId, purchaseOptions.extraData, purchaseOptions.quantity));
- };
-
- useEffect(() =>
- {
- if(!currentOffer) return;
-
- setPurchaseState(CatalogPurchaseState.NONE);
- }, [ currentOffer, setPurchaseOptions ]);
-
- useEffect(() =>
- {
- let timeout: ReturnType = null;
-
- if((purchaseState === CatalogPurchaseState.CONFIRM) || (purchaseState === CatalogPurchaseState.FAILED))
- {
- timeout = setTimeout(() => setPurchaseState(CatalogPurchaseState.NONE), 3000);
- }
-
- return () =>
- {
- if(timeout) clearTimeout(timeout);
- };
- }, [ purchaseState ]);
-
- // Builders-club state — derived + hooks MUST run unconditionally on
- // every render so the hook order stays stable even when currentOffer
- // is null (the `if(!currentOffer) return null` below would otherwise
- // hide the useMemo/useEffect block from the first render and React
- // would flag "Rendered more hooks than during the previous render").
- const isBuildersClubOffer = (currentType === CatalogType.BUILDER);
- const isBuildersClubPlaceable = isBuildersClubOffer
- && !!currentOffer
- && !!currentOffer.product
- && ((currentOffer.product.productType === ProductTypeEnum.FLOOR) || (currentOffer.product.productType === ProductTypeEnum.WALL));
- const builderPlaceableStatus = useMemo(() =>
- {
- if(!isBuildersClubPlaceable || !getBuilderFurniPlaceableStatus || !currentOffer) return BuilderFurniPlaceableStatus.OKAY;
-
- return getBuilderFurniPlaceableStatus(currentOffer);
- }, [ currentOffer, getBuilderFurniPlaceableStatus, isBuildersClubPlaceable, builderPlaceableRefreshTick ]);
- const buildersClubPlaceOneButtonStyle = useMemo(() => ({
- background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)',
- borderColor: '#d79d2e',
- color: '#ffffff'
- }), []);
-
- useEffect(() =>
- {
- if(!isBuildersClubPlaceable) return;
-
- const interval = setInterval(() => setBuilderPlaceableRefreshTick(prevValue => (prevValue + 1)), 500);
-
- return () => clearInterval(interval);
- }, [ isBuildersClubPlaceable ]);
-
- if(!currentOffer) return null;
-
- const PurchaseButton = () =>
- {
- if(isBuildersClubPlaceable)
- {
- const hasMissingExtraParam = (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length));
- const isBlockedByVisitors = (builderPlaceableStatus === BuilderFurniPlaceableStatus.VISITORS_IN_ROOM);
- const isDisabled = hasMissingExtraParam
- || isBlockedByVisitors
- || (builderPlaceableStatus === BuilderFurniPlaceableStatus.MISSING_OFFER)
- || (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_IN_ROOM)
- || (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_ROOM_OWNER)
- || (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN);
- const startBuilderPlacement = (placeMultiple: boolean) =>
- {
- if(builderPlaceableStatus === BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED)
- {
- showSingleBubble(LocalizeText('room.error.max_furniture'), NotificationBubbleType.INFO);
- return;
- }
-
- if(isDisabled) return;
-
- setCatalogPlaceMultipleObjects(placeMultiple);
- requestOfferToMover(currentOffer);
- };
-
- return (
-
-
-
-
-
- { isBlockedByVisitors &&
-
- { LocalizeText('builder.placement_widget.error.visitors') }
- }
- { (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN) &&
-
- { LocalizeText('builder.placement_widget.error.not_group_admin') }
- }
-
- );
- }
-
- const priceCredits = (currentOffer.priceInCredits * purchaseOptions.quantity);
- const pricePoints = (currentOffer.priceInActivityPoints * purchaseOptions.quantity);
-
- if(GetClubMemberLevel() < currentOffer.clubLevel) return ;
-
- if(isLimitedSoldOut) return ;
-
- if(priceCredits > getCurrencyAmount(-1)) return ;
-
- if(pricePoints > getCurrencyAmount(currentOffer.activityPointType)) return ;
-
- switch(purchaseState)
- {
- case CatalogPurchaseState.CONFIRM:
- return ;
- case CatalogPurchaseState.PURCHASE:
- return ;
- case CatalogPurchaseState.FAILED:
- return ;
- case CatalogPurchaseState.SOLD_OUT:
- return ;
- case CatalogPurchaseState.NONE:
- default:
- return ;
- }
- };
-
- return (
- <>
-
- { (!isBuildersClubOffer && !noGiftOption && !currentOffer.isRentOffer) &&
- }
- >
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogSimplePriceWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogSimplePriceWidgetView.tsx
deleted file mode 100644
index c34fe4a..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogSimplePriceWidgetView.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { FC } from 'react';
-import { useCatalogData } from '../../../../../hooks';
-import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
-
-export const CatalogSimplePriceWidgetView: FC<{}> = props =>
-{
- const { currentOffer = null } = useCatalogData();
-
- return (
-
-
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogSingleViewWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogSingleViewWidgetView.tsx
deleted file mode 100644
index c517acd..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogSingleViewWidgetView.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { FC } from 'react';
-import { CatalogFirstProductSelectorWidgetView } from './CatalogFirstProductSelectorWidgetView';
-
-export const CatalogSingleViewWidgetView: FC<{}> = props =>
-{
- return ;
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogSpacesWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogSpacesWidgetView.tsx
deleted file mode 100644
index b100cbe..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogSpacesWidgetView.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { FC, useEffect, useRef, useState } from 'react';
-import { IPurchasableOffer, LocalizeText, Offer, ProductTypeEnum } from '../../../../../api';
-import { AutoGrid, AutoGridProps, Button } from '../../../../../common';
-import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
-import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
-
-interface CatalogSpacesWidgetViewProps extends AutoGridProps
-{
-
-}
-
-const SPACES_GROUP_NAMES = [ 'floors', 'walls', 'views' ];
-
-export const CatalogSpacesWidgetView: FC = props =>
-{
- const { columnCount = 5, children = null, ...rest } = props;
- const [ groupedOffers, setGroupedOffers ] = useState(null);
- const [ selectedGroupIndex, setSelectedGroupIndex ] = useState(-1);
- const [ selectedOfferForGroup, setSelectedOfferForGroup ] = useState(null);
- const { currentPage = null, currentOffer = null } = useCatalogData();
- const { setCurrentOffer = null, setPurchaseOptions = null } = useCatalogUiState();
- const elementRef = useRef(null);
-
- const setSelectedOffer = (offer: IPurchasableOffer) =>
- {
- if(!offer) return;
-
- setSelectedOfferForGroup(prevValue =>
- {
- const newValue = [ ...prevValue ];
-
- newValue[selectedGroupIndex] = offer;
-
- return newValue;
- });
- };
-
- useEffect(() =>
- {
- if(!currentPage) return;
-
- const groupedOffers: IPurchasableOffer[][] = [ [], [], [] ];
-
- for(const offer of currentPage.offers)
- {
- if((offer.pricingModel !== Offer.PRICING_MODEL_SINGLE) && (offer.pricingModel !== Offer.PRICING_MODEL_MULTI)) continue;
-
- const product = offer.product;
-
- if(!product || ((product.productType !== ProductTypeEnum.WALL) && (product.productType !== ProductTypeEnum.FLOOR)) || !product.furnitureData) continue;
-
- const className = product.furnitureData.className;
-
- switch(className)
- {
- case 'floor':
- groupedOffers[0].push(offer);
- break;
- case 'wallpaper':
- groupedOffers[1].push(offer);
- break;
- case 'landscape':
- groupedOffers[2].push(offer);
- break;
- }
- }
-
- setGroupedOffers(groupedOffers);
- setSelectedGroupIndex(0);
- setSelectedOfferForGroup([ groupedOffers[0][0], groupedOffers[1][0], groupedOffers[2][0] ]);
- }, [ currentPage ]);
-
- useEffect(() =>
- {
- if((selectedGroupIndex === -1) || !selectedOfferForGroup) return;
-
- setCurrentOffer(selectedOfferForGroup[selectedGroupIndex]);
-
- }, [ selectedGroupIndex, selectedOfferForGroup, setCurrentOffer ]);
-
- useEffect(() =>
- {
- if((selectedGroupIndex === -1) || !selectedOfferForGroup || !currentOffer) return;
-
- setPurchaseOptions(prevValue =>
- {
- const newValue = { ...prevValue };
-
- newValue.extraData = selectedOfferForGroup[selectedGroupIndex].product.extraParam;
- newValue.extraParamRequired = true;
-
- return newValue;
- });
- }, [ currentOffer, selectedGroupIndex, selectedOfferForGroup, setPurchaseOptions ]);
-
- useEffect(() =>
- {
- if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
- }, [ selectedGroupIndex ]);
-
- if(!groupedOffers || (selectedGroupIndex === -1)) return null;
-
- const offers = groupedOffers[selectedGroupIndex];
-
- return (
- <>
-
- { SPACES_GROUP_NAMES.map((name, index) => ) }
-
-
- { offers && (offers.length > 0) && offers.map((offer, index) => setSelectedOffer(offer) } />) }
- { children }
-
- >
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogSpinnerWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogSpinnerWidgetView.tsx
deleted file mode 100644
index 573465e..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogSpinnerWidgetView.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { FC } from 'react';
-import { FaMinus, FaPlus } from 'react-icons/fa';
-import { LocalizeText } from '../../../../../api';
-import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
-
-const MIN_VALUE: number = 1;
-const MAX_VALUE: number = 99;
-
-export const CatalogSpinnerWidgetView: FC<{}> = props =>
-{
- const { currentOffer = null } = useCatalogData();
- const { purchaseOptions = null, setPurchaseOptions = null } = useCatalogUiState();
- const { quantity = 1 } = purchaseOptions;
-
- const updateQuantity = (value: number) =>
- {
- if(isNaN(value)) value = 1;
-
- value = Math.max(value, MIN_VALUE);
- value = Math.min(value, MAX_VALUE);
-
- if(value === quantity) return;
-
- setPurchaseOptions(prevValue =>
- {
- const newValue = { ...prevValue };
-
- newValue.quantity = value;
-
- return newValue;
- });
- };
-
- if(!currentOffer || !currentOffer.bundlePurchaseAllowed) return null;
-
- return (
-
-
{ LocalizeText('catalog.bundlewidget.spinner.select.amount') }
-
-
- updateQuantity(event.target.valueAsNumber) }
- />
-
-
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogTotalPriceWidget.tsx b/src/components/catalog-modern/views/page/widgets/CatalogTotalPriceWidget.tsx
deleted file mode 100644
index 2ad6129..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogTotalPriceWidget.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { FC } from 'react';
-import { Column, ColumnProps } from '../../../../../common';
-import { useCatalogData } from '../../../../../hooks';
-import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
-
-interface CatalogSimplePriceWidgetViewProps extends ColumnProps
-{
-
-}
-export const CatalogTotalPriceWidget: FC = props =>
-{
- const { gap = 1, ...rest } = props;
- const { currentOffer = null } = useCatalogData();
-
- return (
-
-
-
- );
-};
diff --git a/src/components/catalog-modern/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog-modern/views/page/widgets/CatalogViewProductWidgetView.tsx
deleted file mode 100644
index 46c0184..0000000
--- a/src/components/catalog-modern/views/page/widgets/CatalogViewProductWidgetView.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import { GetAvatarRenderManager, GetSessionDataManager, Vector3d } from '@nitrots/nitro-renderer';
-import { FC, useEffect } from 'react';
-import { BuildPurchasableClothingFigure, FurniCategory, Offer, ProductTypeEnum } from '../../../../../api';
-import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common';
-import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
-
-export const CatalogViewProductWidgetView: FC<{}> = props =>
-{
- const { currentOffer = null, roomPreviewer = null } = useCatalogData();
- const { purchaseOptions = null } = useCatalogUiState();
- const { previewStuffData = null } = purchaseOptions;
-
- useEffect(() =>
- {
- if(!currentOffer || (currentOffer.pricingModel === Offer.PRICING_MODEL_BUNDLE) || !roomPreviewer) return;
-
- const product = currentOffer.product;
-
- if(!product) return;
-
- roomPreviewer.reset(false);
- roomPreviewer.updateObjectRoom('111', '217', '1.1');
- roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
-
- const populate = () =>
- {
- switch(product.productType)
- {
- case ProductTypeEnum.FLOOR: {
- if(!product.furnitureData) return;
-
- const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id);
- const isPurchasableClothing = (product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET);
- const hasResolvableFigureSets = (() =>
- {
- if(!furniData || !furniData.customParams || !furniData.customParams.length) return false;
-
- const parts = furniData.customParams.split(',').map(value => parseInt(value));
-
- for(const part of parts)
- {
- if(isNaN(part)) continue;
-
- if(GetAvatarRenderManager().structureData?.getFigurePartSet(part)) return true;
- }
-
- return false;
- })();
-
- if(isPurchasableClothing || hasResolvableFigureSets)
- {
- const customParts = furniData.customParams.split(',').map(value => parseInt(value));
- const figureSets: number[] = [];
-
- for(const part of customParts)
- {
- if(isNaN(part)) continue;
-
- if(GetAvatarRenderManager().isValidFigureSetForGender(part, GetSessionDataManager().gender)) figureSets.push(part);
- }
-
- const figureString = BuildPurchasableClothingFigure(GetSessionDataManager().figure, figureSets);
-
- roomPreviewer.addAvatarIntoRoom(figureString, product.productClassId);
- }
- else
- {
- roomPreviewer.addFurnitureIntoRoom(product.productClassId, new Vector3d(90), previewStuffData, product.extraParam);
- }
- return;
- }
- case ProductTypeEnum.WALL: {
- if(!product.furnitureData) return;
-
- roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
-
- switch(product.furnitureData.specialType)
- {
- case FurniCategory.FLOOR:
- roomPreviewer.updateObjectRoom(product.extraParam);
- return;
- case FurniCategory.WALL_PAPER:
- roomPreviewer.updateObjectRoom(null, product.extraParam);
- return;
- case FurniCategory.LANDSCAPE: {
- roomPreviewer.updateObjectRoom(null, null, product.extraParam);
-
- const furniData = GetSessionDataManager().getWallItemDataByName('window_double_default');
-
- if(furniData) roomPreviewer.addWallItemIntoRoom(furniData.id, new Vector3d(90), furniData.customParams);
- return;
- }
- default:
- roomPreviewer.updateObjectRoom('101', '101', '1.1');
- roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam);
- return;
- }
- }
- case ProductTypeEnum.ROBOT:
- roomPreviewer.addAvatarIntoRoom(product.extraParam, 0);
- return;
- case ProductTypeEnum.EFFECT:
- roomPreviewer.addAvatarIntoRoom(GetSessionDataManager().figure, product.productClassId);
- return;
- }
- };
-
- populate();
- roomPreviewer.setAutomaticStateChange(false);
- }, [ currentOffer, previewStuffData, roomPreviewer ]);
-
- if(!currentOffer) return null;
-
- if(currentOffer.pricingModel === Offer.PRICING_MODEL_BUNDLE)
- {
- return (
-
-
- { (currentOffer.products.length > 0) && currentOffer.products.map((product, index) =>
- {
- return ;
- }) }
-
-
- );
- }
-
- return ;
-};
diff --git a/src/components/catalog-modern/views/targeted-offer/OfferBubbleView.tsx b/src/components/catalog-modern/views/targeted-offer/OfferBubbleView.tsx
deleted file mode 100644
index 170aeec..0000000
--- a/src/components/catalog-modern/views/targeted-offer/OfferBubbleView.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { TargetedOfferData } from '@nitrots/nitro-renderer';
-import { Dispatch, SetStateAction } from 'react';
-import { GetConfigurationValue } from '../../../../api';
-import { LayoutNotificationBubbleView, Text } from '../../../../common';
-
-export const OfferBubbleView = (props: { offer: TargetedOfferData, setOpen: Dispatch> }) =>
-{
- const { offer = null, setOpen = null } = props;
-
- if(!offer) return;
-
- return setOpen(true) } onClose={ null }>
-
- { offer.title }
- ;
-};
diff --git a/src/components/catalog-modern/views/targeted-offer/OfferView.tsx b/src/components/catalog-modern/views/targeted-offer/OfferView.tsx
deleted file mode 100644
index 0ec2637..0000000
--- a/src/components/catalog-modern/views/targeted-offer/OfferView.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { GetTargetedOfferComposer, TargetedOfferData, TargetedOfferEvent } from '@nitrots/nitro-renderer';
-import { useState } from 'react';
-import { useNitroQuery } from '../../../../api/nitro-query';
-import { OfferBubbleView } from './OfferBubbleView';
-import { OfferWindowView } from './OfferWindowView';
-
-export const OfferView = () =>
-{
- const { data: offer } = useNitroQuery({
- key: [ 'nitro', 'catalog', 'targeted-offer' ],
- request: () => new GetTargetedOfferComposer(),
- parser: TargetedOfferEvent,
- select: evt => evt.getParser()?.data ?? null,
- staleTime: Infinity
- });
-
- const [ opened, setOpened ] = useState(false);
-
- if(!offer) return null;
-
- return (
- <>
- { opened
- ?
- : }
- >
- );
-};
diff --git a/src/components/catalog-modern/views/targeted-offer/OfferWindowView.tsx b/src/components/catalog-modern/views/targeted-offer/OfferWindowView.tsx
deleted file mode 100644
index 72f0658..0000000
--- a/src/components/catalog-modern/views/targeted-offer/OfferWindowView.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { GetTargetedOfferComposer, PurchaseTargetedOfferComposer, TargetedOfferData } from '@nitrots/nitro-renderer';
-import { Dispatch, SetStateAction, useMemo, useState } from 'react';
-import { FriendlyTime, GetConfigurationValue, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../api';
-import { Button, Column, Flex, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
-import { usePurse } from '../../../../hooks';
-
-let isBuyingOffer = false;
-
-export const OfferWindowView = (props: { offer: TargetedOfferData, setOpen: Dispatch> }) =>
-{
- const { offer = null, setOpen = null } = props;
-
- const { getCurrencyAmount } = usePurse();
-
- const [ amount, setAmount ] = useState(1);
-
- const canPurchase = useMemo(() =>
- {
- let credits = false;
- let points = false;
- let limit = false;
-
- if(offer.priceInCredits > 0) credits = getCurrencyAmount(-1) >= offer.priceInCredits;
-
- if(offer.priceInActivityPoints > 0) points = getCurrencyAmount(offer.activityPointType) >= offer.priceInActivityPoints;
- else points = true;
-
- if(offer.purchaseLimit > 0) limit = true;
-
- return (credits && points && limit);
- }, [ offer, getCurrencyAmount ]);
-
- const expirationTime = () =>
- {
- let expirationTime = Math.max(0, (offer.expirationTime - Date.now()) / 1000);
-
- return FriendlyTime.format(expirationTime);
- };
-
- const buyOffer = () =>
- {
- if(isBuyingOffer) return;
-
- isBuyingOffer = true;
-
- SendMessageComposer(new PurchaseTargetedOfferComposer(offer.id, amount));
- SendMessageComposer(new GetTargetedOfferComposer());
-
- setTimeout(() => isBuyingOffer = false, 5000);
- };
-
- if(!offer) return;
-
- return
- setOpen(false) } />
-
- { LocalizeText('targeted.offer.timeleft', [ 'timeleft' ], [ expirationTime() ]) }
-
-
-
-
-
-
- { LocalizeText(offer.title) }
-
-
-
-
- { offer.purchaseLimit > 1 &&
-
- { LocalizeText('catalog.bundlewidget.quantity') }
- setAmount(parseInt(evt.target.value)) } />
-
}
-
-
-
-
-
-
- { LocalizeText('targeted.offer.price.label') }
- { offer.priceInCredits > 0 &&
-
- { offer.priceInCredits }
-
-
}
- { offer.priceInActivityPoints > 0 &&
-
- +{ offer.priceInActivityPoints }
-
}
-
-
- ;
-};
diff --git a/src/components/catalog/CatalogModernView.tsx b/src/components/catalog/CatalogModernView.tsx
deleted file mode 100644
index ea1c2de..0000000
--- a/src/components/catalog/CatalogModernView.tsx
+++ /dev/null
@@ -1,335 +0,0 @@
-import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
-import { FC, useEffect, useState } from 'react';
-import { FaCog, FaEdit, FaEye, FaEyeSlash, FaHeart, FaPlus, FaStar, FaTrash } from 'react-icons/fa';
-import { CatalogType, LocalizeText } from '../../api';
-import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
-import { useCatalogActions, useCatalogData, useCatalogFavorites, useCatalogUiState, useHasPermission } from '../../hooks';
-import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext';
-import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView';
-import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView';
-import { CatalogBuildersClubStatusView } from './views/catalog-header/CatalogBuildersClubStatusView';
-import { CatalogIconView } from './views/catalog-icon/CatalogIconView';
-import { CatalogFavoritesView } from './views/favorites/CatalogFavoritesView';
-import { CatalogGiftView } from './views/gift/CatalogGiftView';
-import { CatalogNavigationView } from './views/navigation/CatalogNavigationView';
-import { CatalogSearchView } from './views/page/common/CatalogSearchView';
-import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout';
-import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView';
-
-const CatalogModernViewInner: FC<{}> = () =>
-{
- const { rootNode = null, currentPage = null, searchResult = null } = useCatalogData();
- const { isVisible = false, setIsVisible = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], setSearchResult = null, currentType = CatalogType.NORMAL } = useCatalogUiState();
- const { openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null } = useCatalogActions();
- const catalogAdmin = useCatalogAdmin();
- const adminMode = catalogAdmin?.adminMode ?? false;
- const setAdminMode = catalogAdmin?.setAdminMode ?? (() =>
- {});
- const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
- const publishCatalog = catalogAdmin?.publishCatalog ?? (() =>
- {});
- const loading = catalogAdmin?.loading ?? false;
- const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites();
- const [ showFavorites, setShowFavorites ] = useState(false);
-
- const isMod = useHasPermission('acc_catalogfurni');
- const totalFavs = favoriteOfferIds.length + favoritePageIds.length;
- const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER)
- ? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' }
- : undefined;
- // Desktop = fixed 780x520. On mobile the window clamps below the viewport so
- // it reads as a dialog (with margins) instead of filling the whole phone
- // screen — applies to both the normal catalog and the Builders Club.
- const catalogCardSize = 'w-[780px] h-[520px] max-w-[96vw] max-h-[72vh] sm:max-w-[100vw] sm:max-h-[92vh]';
-
- useEffect(() =>
- {
- const getCatalogTypeFromLink = (type?: string) =>
- {
- switch((type || '').toLowerCase())
- {
- case 'bc':
- case 'builder':
- case 'buildersclub':
- case 'builders_club':
- return CatalogType.BUILDER;
- default:
- return CatalogType.NORMAL;
- }
- };
-
- const linkTracker: ILinkEventTracker = {
- linkReceived: (url: string) =>
- {
- const parts = url.split('/');
-
- if(parts.length < 2) return;
-
- switch(parts[1])
- {
- case 'show':
- if(parts.length > 2)
- {
- openCatalogByType(getCatalogTypeFromLink(parts[2]));
-
- return;
- }
-
- setIsVisible(true);
- return;
- case 'hide':
- setIsVisible(false);
- return;
- case 'toggle':
- if(parts.length > 2)
- {
- toggleCatalogByType(getCatalogTypeFromLink(parts[2]));
-
- return;
- }
-
- setIsVisible(prevValue => !prevValue);
- return;
- case 'open':
- if(parts.length > 2)
- {
- if(parts.length === 4)
- {
- switch(parts[2])
- {
- case 'offerId':
- openPageByOfferId(parseInt(parts[3]));
- return;
- }
- }
- else
- {
- openPageByName(parts[2]);
- }
- }
- else
- {
- setIsVisible(true);
- }
-
- return;
- }
- },
- eventUrlPrefix: 'catalog/'
- };
-
- AddLinkEventTracker(linkTracker);
-
- return () => RemoveLinkEventTracker(linkTracker);
- }, [ setIsVisible, openPageByOfferId, openPageByName, openCatalogByType, toggleCatalogByType ]);
-
- return (
- <>
- { isVisible &&
-
- setIsVisible(false) } style={ buildersClubHeaderStyle } />
-
- { /* Admin banner */ }
- { adminMode &&
-
- ⚙ Admin Mode
-
-
}
-
-
-
- { /* === LEFT SIDEBAR === */ }
-
-
- { /* Favorites toggle */ }
-
setShowFavorites(!showFavorites) }
- >
-
- 0 ? 'text-danger' : 'text-muted' }` } />
- { totalFavs > 0 &&
-
- { totalFavs }
- }
-
-
{ LocalizeText('catalog.favorites') }
-
-
-
-
- { /* Admin: root page actions */ }
- { adminMode && rootNode &&
-
-
-
-
}
-
- { /* Category icons */ }
- { rootNode && rootNode.children.length > 0 && rootNode.children.map((child, index) =>
- {
- if(!adminMode && !child.isVisible) return null;
-
- const isHidden = !child.isVisible;
-
- return (
-
- {
- if(searchResult) setSearchResult(null);
- if(showFavorites) setShowFavorites(false);
- activateNode(child);
- } }
- >
-
-
- { isHidden && }
-
-
- { child.localization }
-
- { /* Admin actions on each root category */ }
- { adminMode &&
-
-
- {
- e.stopPropagation();
- catalogAdmin.setEditingPageNode(child);
- catalogAdmin.setEditingRootPage(false);
- catalogAdmin.setEditingPageData(true);
- } }
- >
-
-
-
- {
- e.stopPropagation();
- catalogAdmin.togglePageVisible(child.pageId);
- } }
- >
- { isHidden
- ?
- : }
-
-
- {
- e.stopPropagation();
- if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ])))
- {
- catalogAdmin.deletePage(child.pageId);
- }
- } }
- >
-
-
-
}
-
- );
- }) }
-
-
- { /* === MAIN AREA === */ }
-
- { /* Toolbar: search + admin */ }
-
- { /* Breadcrumb */ }
-
-
- { activeNodes && activeNodes.length > 0
- ? activeNodes.map((node, i) => (
-
- { i > 0 && › }
- activateNode(node) : undefined }>
- { node.localization }
-
-
- ))
- : { LocalizeText('catalog.title') } }
-
-
-
-
-
-
- { isMod &&
-
}
-
-
- { /* Content area */ }
-
- { showFavorites
- ?
- setShowFavorites(false) } />
-
- : <>
- { !navigationHidden && activeNodes && activeNodes.length > 0 &&
-
-
-
}
-
- { adminMode && }
- { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) }
-
- > }
-
-
-
-
- }
-
-
-
- >
- );
-};
-
-export const CatalogModernView: FC<{}> = () =>
-{
- return (
-
-
-
- );
-};
diff --git a/src/components/catalog/CatalogView.tsx b/src/components/catalog/CatalogView.tsx
index 3a4527c..fbc700e 100644
--- a/src/components/catalog/CatalogView.tsx
+++ b/src/components/catalog/CatalogView.tsx
@@ -1,26 +1,10 @@
import { FC } from 'react';
-import { useCatalogClassicStyle, useCatalogData } from '../../hooks';
+import { useCatalogData } from '../../hooks';
import { CatalogClassicView } from './CatalogClassicView';
-// Modern catalog FULLY FORKED into ../catalog-modern/* (own copy of every catalog
-// component) so the two catalogs share NOTHING: editing the modern one never
-// touches duckie's classic, which stays 1:1 upstream.
-import { CatalogModernView } from '../catalog-modern/CatalogModernView';
export const CatalogView: FC<{}> = () =>
{
const { catalogLocalizationVersion = 0 } = useCatalogData();
- const [ catalogClassicStyle ] = useCatalogClassicStyle();
-
- // Default (toggle OFF) = duckie's classic catalog 1:1 upstream (./CatalogClassicView,
- // uses the original ./views/* tree). Toggle ON = the modern catalog, a fully
- // self-contained fork under ../catalog-modern/* — nothing shared between the two.
- // Both the normal catalog and the Builders Club follow this toggle.
- if(catalogClassicStyle) return (
- <>
-
-
- >
- );
return (
<>
diff --git a/src/components/navigator/views/search/NavigatorSearchSavesResultItemView.tsx b/src/components/navigator/views/search/NavigatorSearchSavesResultItemView.tsx
index b8f5dfa..27392ff 100644
--- a/src/components/navigator/views/search/NavigatorSearchSavesResultItemView.tsx
+++ b/src/components/navigator/views/search/NavigatorSearchSavesResultItemView.tsx
@@ -25,6 +25,12 @@ export const NavigatorSearchSavesResultItemView: FC
{
const code = search.code.split('.').reverse()[0];
diff --git a/src/components/purse/PurseClassicView.tsx b/src/components/purse/PurseClassicView.tsx
index f183e69..d8f6408 100644
--- a/src/components/purse/PurseClassicView.tsx
+++ b/src/components/purse/PurseClassicView.tsx
@@ -84,7 +84,8 @@ export const PurseClassicView: FC<{}> = props =>
body: JSON.stringify({ ssoTicket, rememberToken })
});
}
- catch {}
+ catch
+ { /* best-effort — proceed with local logout regardless */ }
ClearRememberLogin();
if(window.NitroConfig) window.NitroConfig['sso.ticket'] = '';
diff --git a/src/components/purse/PurseModernView.tsx b/src/components/purse/PurseModernView.tsx
index adbd851..e3579fd 100644
--- a/src/components/purse/PurseModernView.tsx
+++ b/src/components/purse/PurseModernView.tsx
@@ -81,7 +81,8 @@ export const PurseModernView: FC<{}> = props =>
body: JSON.stringify({ ssoTicket, rememberToken })
});
}
- catch { }
+ catch
+ { /* best-effort — proceed with local logout regardless */ }
ClearRememberLogin();
if(window.NitroConfig) window.NitroConfig['sso.ticket'] = '';
diff --git a/src/components/purse/PurseView.tsx b/src/components/purse/PurseView.tsx
index 32362e6..b039ebd 100644
--- a/src/components/purse/PurseView.tsx
+++ b/src/components/purse/PurseView.tsx
@@ -1,11 +1,7 @@
import { FC } from 'react';
-import { useCatalogClassicStyle } from '../../hooks';
-import { PurseClassicView } from './PurseClassicView';
import { PurseModernView } from './PurseModernView';
export const PurseView: FC<{}> = props =>
{
- const [ classicStyle ] = useCatalogClassicStyle();
-
- return classicStyle ? : ;
+ return ;
};
diff --git a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx
index 5c88bda..459a6fc 100644
--- a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx
+++ b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx
@@ -7,25 +7,16 @@ interface ChatInputCommandSelectorViewProps
selectedIndex: number;
onSelect: (command: CommandDefinition) => void;
onHover: (index: number) => void;
- /**
- * When true, render the flat minimalist look (gray list, dark-blue
- * selection). When false / undefined (default) the picker wears the
- * Habbo NitroCard chrome with the green :command header strip.
- */
- newStyle?: boolean;
}
/**
- * :command autocomplete popover. Two visual modes, both driven by the
- * "New style" toggle in user settings (memenu.settings.other.catalog.classic.style):
- *
- * - newStyle = false (default): cream cardstock, habbo-green header,
- * UbuntuCondensed names, green ":" tile, custom Habbo scrollbar.
- * - newStyle = true: flat gray list, dark-blue selection, plain text rows.
+ * :command autocomplete popover. Wears the Habbo NitroCard chrome: cream
+ * cardstock, habbo-green header, UbuntuCondensed names, green ":" tile and
+ * the custom Habbo scrollbar.
*/
export const ChatInputCommandSelectorView: FC = props =>
{
- const { commands = [], selectedIndex = 0, onSelect = null, onHover = null, newStyle = false } = props;
+ const { commands = [], selectedIndex = 0, onSelect = null, onHover = null } = props;
const listRef = useRef(null);
useEffect(() =>
@@ -37,25 +28,6 @@ export const ChatInputCommandSelectorView: FC
if(selected) selected.scrollIntoView({ block: 'nearest' });
}, [ selectedIndex ]);
- if(newStyle)
- {
- return (
-
- { commands.map((cmd, index) => (
-
onSelect(cmd) }
- onMouseEnter={ () => onHover(index) }
- >
- :{ cmd.key }
- { cmd.description }
-
- )) }
-
- );
- }
-
return (
diff --git a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx
index 8a67c8a..a7dc5c1 100644
--- a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx
+++ b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx
@@ -8,28 +8,16 @@ interface ChatInputMentionSelectorViewProps
selectedIndex: number;
onSelect: (suggestion: MentionSuggestion) => void;
onHover: (index: number) => void;
- /**
- * When true, render the flat minimalist look (gray list, dark-blue
- * selection, no header / no kind chip). When false / undefined (default)
- * the picker wears the Habbo NitroCard chrome.
- */
- newStyle?: boolean;
}
/**
- * @-mention autocomplete popover. Two visual modes, both driven by the
- * "New style" toggle in user settings (memenu.settings.other.catalog.classic.style):
- *
- * - newStyle = false (default): cream cardstock, habbo-blue header,
- * UbuntuCondensed names, kind chips, custom Habbo scrollbar.
- * - newStyle = true: flat gray list, dark-blue selection, plain text rows.
- *
- * Both modes share the same suggestion structure and keyboard contract -
- * the difference is purely cosmetic.
+ * @-mention autocomplete popover. Wears the Habbo NitroCard chrome: cream
+ * cardstock, habbo-blue header, UbuntuCondensed names, kind chips and the
+ * custom Habbo scrollbar.
*/
export const ChatInputMentionSelectorView: FC
= props =>
{
- const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null, newStyle = false } = props;
+ const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null } = props;
const listRef = useRef(null);
useEffect(() =>
@@ -43,45 +31,6 @@ export const ChatInputMentionSelectorView: FC
if(suggestions.length === 0) return null;
- if(newStyle)
- {
- return (
-
- { suggestions.map((suggestion, index) =>
- {
- const isSelected = (index === selectedIndex);
- const rowClass = `px-3 py-1.5 cursor-pointer text-sm flex items-center gap-2 ${ isSelected ? 'bg-[#283F5D] text-white' : 'hover:bg-gray-300' }`;
-
- return (
-
onSelect(suggestion) }
- onMouseEnter={ () => onHover(index) }
- >
- { suggestion.kind === 'user' && suggestion.figure
- ? (
-
-
-
- )
- : (
-
@
- ) }
-
@{ suggestion.name }
- { suggestion.description &&
{ suggestion.description } }
-
- );
- }) }
-
- );
- }
-
return (
diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx
index 0997b3e..1edcfe5 100644
--- a/src/components/room/widgets/chat-input/ChatInputView.tsx
+++ b/src/components/room/widgets/chat-input/ChatInputView.tsx
@@ -3,7 +3,7 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, RoomWidgetUpdateChatInputContentEvent } from '../../../../api';
import { Text } from '../../../../common';
-import { useCatalogClassicStyle, useChatCommandSelector, useChatInputWidget, useChatMentions, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks';
+import { useChatCommandSelector, useChatInputWidget, useChatMentions, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks';
import { ChatInputCommandSelectorView } from './ChatInputCommandSelectorView';
import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView';
import { ChatInputMentionSelectorView } from './ChatInputMentionSelectorView';
@@ -18,12 +18,6 @@ export const ChatInputView: FC<{}> = props =>
const inputRef = useRef
(null);
const { isVisible: commandSelectorVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close: closeCommandSelector } = useChatCommandSelector(chatValue);
- // The "New style" user-setting (memenu.settings.other.catalog.classic.style)
- // drives BOTH the catalog layout and the mention-picker chrome:
- // false (default) = Habbo old-school NitroCard cardstock look
- // true = flat minimalist gray look
- const [ newStyle ] = useCatalogClassicStyle();
-
const mention = useChatMentions(chatValue, setChatValue, inputRef, commandSelectorVisible);
const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []);
@@ -329,7 +323,6 @@ export const ChatInputView: FC<{}> = props =>
setChatValue(':' + cmd.key + ' '); inputRef.current?.focus();
} }
onHover={ setSelectedIndex }
- newStyle={ newStyle }
/> }
{ mention.visible && !commandSelectorVisible &&
= props =>
selectedIndex={ mention.selectedIndex }
onSelect={ mention.apply }
onHover={ mention.setSelectedIndex }
- newStyle={ newStyle }
/> }
{ !floodBlocked &&
diff --git a/src/components/theme/ThemeApplier.tsx b/src/components/theme/ThemeApplier.tsx
deleted file mode 100644
index 1f02ed1..0000000
--- a/src/components/theme/ThemeApplier.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { FC } from 'react';
-import { useThemes } from '../../hooks';
-
-// Mounted once at app level: subscribing to the shared theme store triggers the
-// load + apply effects, so the saved/default custom theme is applied on boot
-// and kept in sync when the user changes it from Settings. Renders nothing.
-export const ThemeApplier: FC<{}> = () =>
-{
- useThemes();
-
- return null;
-};
diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx
index 106071d..4ed5dbd 100644
--- a/src/components/user-settings/UserSettingsView.tsx
+++ b/src/components/user-settings/UserSettingsView.tsx
@@ -3,19 +3,16 @@ import { FC, useEffect, useState } from 'react';
import { FaUserCog, FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa';
import { DispatchMainEvent, DispatchUiEvent, LocalizeText, SendMessageComposer } from '../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
-import { useCatalogClassicStyle, useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent, useThemes } from '../../hooks';
+import { useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks';
import { classNames } from '../../layout';
export const UserSettingsView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
- const [ activeTab, setActiveTab ] = useState<'general' | 'themes'>('general');
const [ userSettings, setUserSettings ] = useState
(null);
- const { themes, activeThemeId, manifest, activeEnabled, selectTheme, togglePiece } = useThemes();
const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems();
const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation();
const [ chatWindowEnabled, setChatWindowEnabled ] = useChatWindow();
- const [ catalogClassicStyle, setCatalogClassicStyle ] = useCatalogClassicStyle();
const processAction = (type: string, value?: boolean | number | string) =>
{
@@ -134,11 +131,6 @@ export const UserSettingsView: FC<{}> = props =>
processAction('close_view') } />
-
-
-
-
- { activeTab === 'general' && <>
{ LocalizeText('widget.memenu.settings.volume') }
@@ -214,35 +202,6 @@ export const UserSettingsView: FC<{}> = props =>
›
- > }
- { activeTab === 'themes' &&
-
- { LocalizeText('usersettings.themes.custom') }
-
-
- { activeThemeId && manifest && manifest.pieces.length > 0 &&
-
-
{ LocalizeText('usersettings.themes.active_pieces') }
- { manifest.pieces.map(piece => (
-
- togglePiece(piece.id) } />
- { piece.name }
-
- )) }
-
}
- { activeThemeId && !manifest &&
-
{ LocalizeText('usersettings.themes.invalid') } }
- { !themes.length &&
-
{ LocalizeText('usersettings.themes.none') } }
-
}
);
diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css
index 1f9586e..54d978d 100644
--- a/src/css/catalog/CatalogClassicView.css
+++ b/src/css/catalog/CatalogClassicView.css
@@ -1409,6 +1409,9 @@
}
.nitro-catalog-classic-window *::-webkit-scrollbar-thumb {
+ /* Grip: a single 2px #a0a0a0 stripe in an 8px-wide centered band,
+ repeated every 5px (2px stripe + 3px body gap).
+ Outline: 1px black border, then a 2px white inset frame inside it. */
min-height: 24px !important;
background:
url("data:image/svg+xml;utf8,") center top / 8px 5px repeat-y,
@@ -1430,6 +1433,9 @@
#bcbcbc !important;
}
+/* Arrow buttons: light grey cap with rounded OUTER corners (up button
+ rounded at the top, down button rounded at the bottom), 1px black
+ border, dark chevron via inline SVG. */
.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement {
display: block !important;
width: 18px !important;
@@ -1546,6 +1552,13 @@
display: none !important;
}
+ /* Stack the navigation above the furni/preview layout and let the
+ whole content area scroll. The previous grid used
+ `grid-template-rows: auto minmax(0, 1fr)`, but on iOS Safari the
+ flex height chain is indefinite, so the 1fr layout-shell row
+ collapsed to 0 and only the sidebar (category list) was visible.
+ A flex column sized to content + a scrollable content-shell is
+ device-robust. */
.nitro-catalog-classic-stage,
.nitro-catalog-classic-stage.is-navigation-hidden {
display: flex;
@@ -1561,6 +1574,9 @@
max-height: 30vh;
}
+ /* The default layout's children (preview, grid, buy bar) are
+ absolutely positioned against a fixed ~460px box, so give the
+ shell a definite height and never clip it on mobile. */
.nitro-catalog-classic-layout-shell {
flex: 0 0 auto;
width: 100%;
diff --git a/src/css/catalog/CatalogModern.css b/src/css/catalog/CatalogModern.css
deleted file mode 100644
index cabd227..0000000
--- a/src/css/catalog/CatalogModern.css
+++ /dev/null
@@ -1,102 +0,0 @@
-/* ============================================================================
- Catalogo MODERN (Hippiehotel) — override CSS scopati a .nitro-catalog.
- Il CSS catalogo condiviso (CatalogClassicView.css) e' SWF-style block/absolute,
- pensato per la struttura del catalogo CLASSICO. Il catalogo modern
- (CatalogModernView + CatalogLayoutDefaultView in ../catalog-modern/) usa un
- layout FLEX: questi override ripristinano il flex e adattano gli elementi SOLO
- per la finestra modern (.nitro-catalog), lasciando intatto il classico
- (.nitro-catalog-classic-window).
- ============================================================================ */
-
-/* Variabili SWF: definite solo su .nitro-catalog-classic-window → replicate sulla modern */
-.nitro-catalog {
- --catalog-swf-bg: #ecece4;
- --catalog-swf-panel: #f7f7f2;
- --catalog-swf-panel-2: #e7e7df;
- --catalog-swf-border: #9d9d96;
- --catalog-swf-border-dark: #6f6f6a;
- --catalog-swf-text: #222222;
- --catalog-swf-muted: #666666;
- --catalog-swf-blue: #2f8097;
- --catalog-swf-blue-dark: #1c596c;
- --catalog-swf-select: #63c5e9;
- --catalog-swf-select-outer: #82d1ed;
- --catalog-swf-bc: #ff8d00;
- --catalog-swf-bc-outer: #ffb53c;
-}
-
-/* Layout flex (il CSS condiviso forza display:block + posizionamento assoluto) */
-.nitro-catalog .nitro-catalog-classic-default-layout {
- display: flex !important;
- flex-direction: column !important;
- min-height: 0 !important;
-}
-
-.nitro-catalog .nitro-catalog-classic-offer-panel {
- height: auto !important;
- flex: 0 0 auto !important;
-}
-
-.nitro-catalog .nitro-catalog-classic-offer-preview,
-.nitro-catalog .nitro-catalog-classic-offer-panel > .nitro-catalog-classic-offer-preview {
- position: relative !important;
- height: 200px !important;
- width: 300px !important;
- min-width: 300px !important;
-}
-
-.nitro-catalog .nitro-catalog-classic-offer-info {
- display: flex !important;
- padding-left: 22px !important;
- padding-right: 14px !important;
- justify-content: center !important;
-}
-
-.nitro-catalog .nitro-catalog-classic-grid-shell {
- position: static !important;
- inset: auto !important;
- left: auto !important;
- top: auto !important;
- bottom: auto !important;
- right: auto !important;
- width: auto !important;
- flex: 1 1 auto !important;
- min-height: 0 !important;
- overflow: auto !important;
-}
-
-/* Bottoni Rotate/Toggle State: pill leggibili agli angoli del preview, icone bianche */
-.nitro-catalog .nitro-catalog-classic-preview-btn {
- position: absolute !important;
- top: 8px !important;
- width: auto !important;
- min-width: 0 !important;
- height: auto !important;
- min-height: 0 !important;
- padding: 3px 9px !important;
- background: rgba(0, 0, 0, 0.58) !important;
- color: #ffffff !important;
- border: 0 !important;
- border-radius: 4px !important;
- border-image: none !important;
- font-size: 10px !important;
- font-weight: 600 !important;
- line-height: 1.2 !important;
- gap: 4px !important;
- z-index: 4;
-}
-
-.nitro-catalog .nitro-catalog-classic-preview-btn svg,
-.nitro-catalog .nitro-catalog-classic-preview-btn path {
- color: #ffffff !important;
- fill: #ffffff !important;
-}
-
-.nitro-catalog .nitro-catalog-classic-preview-rotate { left: 8px !important; right: auto !important; }
-.nitro-catalog .nitro-catalog-classic-preview-state { right: 8px !important; left: auto !important; }
-
-/* Nome doppio sul preview rimosso → resta quello nell'info (a destra) */
-.nitro-catalog .nitro-catalog-classic-preview-title { display: none !important; }
-
-/* Show pieno senza preview: niente welcome/intro → la griglia prende tutta la pagina */
-.nitro-catalog .nitro-catalog-classic-welcome { display: none !important; }
diff --git a/src/css/purse/PurseClassicView.css b/src/css/purse/PurseClassicView.css
index c92a8ba..c3456af 100644
--- a/src/css/purse/PurseClassicView.css
+++ b/src/css/purse/PurseClassicView.css
@@ -1,11 +1,19 @@
+/* Classic (original) purse style. All selectors are scoped under
+ .nitro-purse-classic so they never collide with the modern PurseView.css
+ rules that share class names like .nitro-purse. */
+
.nitro-purse-classic {
width: 100%;
}
+/* Extra (seasonal) currency in classic mode reuses the modern boxed
+ .nitro-purse__other styling, just constrained to the classic purse width. */
.nitro-purse__other--classic {
max-width: 125px;
}
+/* The #41403c border on the extra-currency box is new-style-only;
+ classic mode has no border. */
.nitro-purse__other--classic .nitro-purse-seasonal-currency {
border: 0;
}
diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css
index bc07713..f6ef678 100644
--- a/src/css/purse/PurseView.css
+++ b/src/css/purse/PurseView.css
@@ -16,6 +16,7 @@
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.14);
}
+/* ---- Body: 3 columns (currencies | Join+Earnings | actions) ---- */
.nitro-purse__body {
display: flex;
gap: 6px;
@@ -23,6 +24,7 @@
padding: 6px;
}
+/* ---- Currencies (left) ---- */
.nitro-purse__currencies {
display: flex;
flex: 1 1 0;
@@ -62,6 +64,7 @@
color: #df95ff !important;
}
+/* ---- Button columns (Join+Earnings and actions) ---- */
.nitro-purse__col {
display: flex;
flex: 0 0 auto;
diff --git a/src/hooks/catalog/index.ts b/src/hooks/catalog/index.ts
index 1b4b9bc..2a817fa 100644
--- a/src/hooks/catalog/index.ts
+++ b/src/hooks/catalog/index.ts
@@ -1,5 +1,4 @@
export * from './useCatalog';
-export * from './useCatalogClassicStyle';
export * from './useCatalogFavorites';
export * from './useCatalogPlaceMultipleItems';
export * from './useCatalogSkipPurchaseConfirmation';
diff --git a/src/hooks/catalog/useCatalogClassicStyle.ts b/src/hooks/catalog/useCatalogClassicStyle.ts
deleted file mode 100644
index a239a60..0000000
--- a/src/hooks/catalog/useCatalogClassicStyle.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useBetween } from 'use-between';
-import { GetConfigurationValue, LocalStorageKeys } from '../../api';
-import { useLocalStorage } from '../useLocalStorage';
-
-// Per-user toggle for the catalog visual style.
-// - true => classic (old) catalog look
-// - false => modern (rebuilt) catalog look
-// The default for users who never touched the toggle comes from the global
-// `catalog.classic.style` flag in ui-config.json, so an admin can flip the
-// default for everyone (true = classic for all, false = modern for all)
-// while still letting each user override it from the settings panel.
-const useCatalogClassicStyleState = () => useLocalStorage(LocalStorageKeys.CATALOG_CLASSIC_STYLE, GetConfigurationValue('catalog.classic.style', false));
-
-export const useCatalogClassicStyle = () => useBetween(useCatalogClassicStyleState);
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 87643b2..9bedaeb 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -25,7 +25,6 @@ export * from './rooms/widgets';
export * from './rooms/widgets/furniture';
export * from './session';
export * from './soundboard/useSoundboard';
-export * from './theme';
export * from './translation';
export * from './useLocalStorage';
export * from './useSharedVisibility';
diff --git a/src/hooks/theme/index.ts b/src/hooks/theme/index.ts
deleted file mode 100644
index effac86..0000000
--- a/src/hooks/theme/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './useThemes';
diff --git a/src/hooks/theme/useThemes.ts b/src/hooks/theme/useThemes.ts
deleted file mode 100644
index c2ef444..0000000
--- a/src/hooks/theme/useThemes.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-import { useBetween } from 'use-between';
-import { ApplyThemePieces, ClearTheme, FetchThemeIndex, FetchThemeManifest, GetConfigurationValue, LocalStorageKeys, ThemeInfo, ThemeManifest } from '../../api';
-import { useLocalStorage } from '../useLocalStorage';
-
-// Per-user custom theme selection.
-// - activeThemeId: '' = default (no custom theme). Default for new users comes
-// from ui-config `theme.default` so the admin can set a hotel-wide default
-// (like catalog.classic.style), while each user can override from Settings.
-// - enabledPieces[themeId]: which graphic pieces of that theme are active
-// (checkboxes). If absent, defaults to ui-config `theme.default.pieces`
-// (when on the default theme) or ALL pieces.
-const useThemesState = () =>
-{
- const [ activeThemeId, setActiveThemeId ] = useLocalStorage(LocalStorageKeys.THEME_ACTIVE, GetConfigurationValue('theme.default', ''));
- const [ enabledPieces, setEnabledPieces ] = useLocalStorage>(LocalStorageKeys.THEME_PIECES, {});
- const [ themes, setThemes ] = useState([]);
- const [ manifest, setManifest ] = useState(null);
- const [ loaded, setLoaded ] = useState(false);
-
- // Load the theme index once.
- useEffect(() =>
- {
- let alive = true;
-
- FetchThemeIndex().then(list =>
- {
- if(alive) setThemes(list);
- }).finally(() =>
- {
- if(alive) setLoaded(true);
- });
-
- return () => { alive = false; };
- }, []);
-
- // Load the manifest whenever the active theme changes.
- useEffect(() =>
- {
- let alive = true;
-
- if(!activeThemeId)
- {
- setManifest(null);
- ClearTheme();
- return;
- }
-
- FetchThemeManifest(activeThemeId).then(m =>
- {
- if(!alive) return;
-
- setManifest(m);
-
- if(!m) ClearTheme(); // broken/missing manifest -> full fallback to default
- });
-
- return () => { alive = false; };
- }, [ activeThemeId ]);
-
- // Which pieces are enabled for the current theme.
- const activeEnabled = useMemo(() =>
- {
- if(!manifest) return [] as string[];
-
- const stored = enabledPieces[activeThemeId];
-
- if(stored) return stored;
-
- const fromConfig = GetConfigurationValue('theme.default.pieces', null);
-
- // Default: config list (if this is the default theme) else every piece on.
- if(fromConfig && activeThemeId === GetConfigurationValue('theme.default', '')) return fromConfig;
-
- return manifest.pieces.map(p => p.id);
- }, [ manifest, enabledPieces, activeThemeId ]);
-
- // Apply (inject/remove s) whenever theme or enabled pieces change.
- useEffect(() =>
- {
- if(!activeThemeId || !manifest)
- {
- ClearTheme();
- return;
- }
-
- ApplyThemePieces(activeThemeId, manifest.pieces.filter(p => activeEnabled.includes(p.id)));
- }, [ activeThemeId, manifest, activeEnabled ]);
-
- const selectTheme = (id: string) => setActiveThemeId(id || '');
-
- const togglePiece = (pieceId: string) =>
- {
- if(!activeThemeId || !manifest) return;
-
- setEnabledPieces(prev =>
- {
- const current = prev[activeThemeId] ?? manifest.pieces.map(p => p.id);
- const next = current.includes(pieceId) ? current.filter(x => x !== pieceId) : [ ...current, pieceId ];
-
- return { ...prev, [activeThemeId]: next };
- });
- };
-
- return { themes, activeThemeId, manifest, activeEnabled, loaded, selectTheme, togglePiece };
-};
-
-export const useThemes = () => useBetween(useThemesState);
diff --git a/src/index.tsx b/src/index.tsx
index ba151ea..3b7af7d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -20,7 +20,6 @@ import './css/index.css';
import './css/backgrounds/BackgroundsView.css';
import './css/badges/BadgeLeaderboardView.css';
import './css/catalog/CatalogClassicView.css';
-import './css/catalog/CatalogModern.css';
import './css/emustats/EmuStatsView.css';
import './css/chat/Chats.css';
@@ -56,8 +55,6 @@ import './css/notification/NotificationCenterView.css';
import './css/purse/PurseView.css';
-import './css/purse/PurseClassicView.css';
-
import './css/room/InfoStand.css';
import './css/room/NavigatorRoomSettings.css';
import './css/room/RoomWidgets.css';