More catalog changes

This commit is contained in:
Bill 2022-02-01 01:58:19 -05:00
parent c19810327d
commit 8129bd3393
64 changed files with 1285 additions and 1346 deletions

View File

@ -2,7 +2,7 @@ import { ApproveNameMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent
import { GuildMembershipsMessageEvent } from '@nitrots/nitro-renderer/src/nitro/communication/messages/incoming/user/GuildMembershipsMessageEvent';
import { FC, useCallback } from 'react';
import { GetFurnitureData, GetProductDataForLocalization, LocalizeText } from '../../api';
import { CatalogNameResultEvent, CatalogPurchaseFailureEvent, CatalogSelectProductEvent, CatalogSetExtraPurchaseParameterEvent } from '../../events';
import { CatalogNameResultEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogSetExtraPurchaseParameterEvent } from '../../events';
import { CatalogGiftReceiverNotFoundEvent } from '../../events/catalog/CatalogGiftReceiverNotFoundEvent';
import { CatalogPurchasedEvent } from '../../events/catalog/CatalogPurchasedEvent';
import { CatalogPurchaseSoldOutEvent } from '../../events/catalog/CatalogPurchaseSoldOutEvent';
@ -14,6 +14,7 @@ import { NotificationUtilities } from '../../views/notification-center/common/No
import { CatalogNode } from './common/CatalogNode';
import { CatalogPetPalette } from './common/CatalogPetPalette';
import { CatalogType } from './common/CatalogType';
import { GiftWrappingConfiguration } from './common/GiftWrappingConfiguration';
import { ICatalogNode } from './common/ICatalogNode';
import { IProduct } from './common/IProduct';
import { IPurchasableOffer } from './common/IPurchasableOffer';
@ -23,11 +24,10 @@ import { Product } from './common/Product';
import { ProductTypeEnum } from './common/ProductTypeEnum';
import { SubscriptionInfo } from './common/SubscriptionInfo';
import { useCatalogContext } from './context/CatalogContext';
import { CatalogActions } from './reducers/CatalogReducer';
export const CatalogMessageHandler: FC<{}> = props =>
{
const { setIsBusy, pageId, currentType, setRootNode, setOffersToNodes, currentPage, setCurrentOffer, setPurchasableOffer, setFrontPageItems, resetState, showCatalogPage, catalogState, dispatchCatalogState } = useCatalogContext();
const { setIsBusy, pageId, currentType, setRootNode, setOffersToNodes, currentPage, setCurrentOffer, setFrontPageItems, resetState, showCatalogPage, setCatalogOptions = null } = useCatalogContext();
const onCatalogPagesListEvent = useCallback((event: CatalogPagesListEvent) =>
{
@ -115,6 +115,8 @@ export const CatalogMessageHandler: FC<{}> = props =>
const onPurchaseNotAllowedMessageEvent = useCallback((event: PurchaseNotAllowedMessageEvent) =>
{
const parser = event.getParser();
dispatchUiEvent(new CatalogPurchaseNotAllowedEvent(parser.code));
}, []);
const onLimitedEditionSoldOutEvent = useCallback((event: LimitedEditionSoldOutEvent) =>
@ -154,7 +156,7 @@ export const CatalogMessageHandler: FC<{}> = props =>
offer.page = currentPage;
dispatchUiEvent(new CatalogSelectProductEvent(offer));
setCurrentOffer(offer);
if(offer.product && (offer.product.productType === ProductTypeEnum.WALL))
{
@ -162,19 +164,36 @@ export const CatalogMessageHandler: FC<{}> = props =>
}
// (this._isObjectMoverRequested) && (this._purchasableOffer)
// setPurchasableOffer(offer);
}, [ currentType, currentPage ]);
}, [ currentType, currentPage, setCurrentOffer ]);
const onSellablePetPalettesMessageEvent = useCallback((event: SellablePetPalettesMessageEvent) =>
{
const parser = event.getParser();
const petPalette = new CatalogPetPalette(parser.productCode, parser.palettes.slice());
dispatchCatalogState({
type: CatalogActions.SET_PET_PALETTE,
payload: { petPalette }
});
}, [ dispatchCatalogState ]);
setCatalogOptions(prevValue =>
{
const petPalettes = [];
if(prevValue.petPalettes) petPalettes.push(...prevValue.petPalettes);
for(let i = 0; i < petPalettes.length; i++)
{
const palette = petPalettes[i];
if(palette.breed === petPalette.breed)
{
petPalettes.splice(i, 1);
break;
}
}
petPalettes.push(petPalette);
return { ...prevValue, petPalettes };
});
}, [ setCatalogOptions ]);
const onApproveNameMessageEvent = useCallback((event: ApproveNameMessageEvent) =>
{
@ -192,43 +211,42 @@ export const CatalogMessageHandler: FC<{}> = props =>
{
const parser = event.getParser();
dispatchCatalogState({
type: CatalogActions.SET_CLUB_OFFERS,
payload: {
clubOffers: parser.offers
}
});
}, [ dispatchCatalogState ]);
setCatalogOptions(prevValue =>
{
const clubOffers = parser.offers;
return { ...prevValue, clubOffers };
});
}, [ setCatalogOptions ]);
const onGuildMembershipsMessageEvent = useCallback((event: GuildMembershipsMessageEvent) =>
{
const parser = event.getParser();
dispatchCatalogState({
type: CatalogActions.SET_GROUPS,
payload: {
groups: parser.groups
}
});
}, [ dispatchCatalogState ]);
setCatalogOptions(prevValue =>
{
const groups = parser.groups;
return { ...prevValue, groups };
});
}, [ setCatalogOptions ]);
const onUserSubscriptionEvent = useCallback((event: UserSubscriptionEvent) =>
{
const parser = event.getParser();
dispatchCatalogState({
type: CatalogActions.SET_SUBSCRIPTION_INFO,
payload: {
subscriptionInfo: new SubscriptionInfo(
setCatalogOptions(prevValue =>
{
const subscriptionInfo = new SubscriptionInfo(
Math.max(0, parser.daysToPeriodEnd),
Math.max(0, parser.periodsSubscribedAhead),
parser.isVip,
parser.pastClubDays,
parser.pastVipDays
)
}
});
}, [ dispatchCatalogState ]);
parser.pastVipDays);
return { ...prevValue, subscriptionInfo };
});
}, [ setCatalogOptions ]);
const onCatalogPublishedMessageEvent = useCallback((event: CatalogPublishedMessageEvent) =>
{
@ -239,13 +257,13 @@ export const CatalogMessageHandler: FC<{}> = props =>
{
const parser = event.getParser();
dispatchCatalogState({
type: CatalogActions.SET_GIFT_CONFIGURATION,
payload: {
giftConfiguration: parser
}
});
}, [ dispatchCatalogState ]);
setCatalogOptions(prevValue =>
{
const giftConfiguration = new GiftWrappingConfiguration(parser);
return { ...prevValue, giftConfiguration };
});
}, [ setCatalogOptions ]);
const onMarketplaceMakeOfferResult = useCallback((event: MarketplaceMakeOfferResult) =>
{
@ -272,29 +290,25 @@ export const CatalogMessageHandler: FC<{}> = props =>
{
const parser = event.getParser();
if(!parser) return;
setCatalogOptions(prevValue =>
{
const marketplaceConfiguration = parser;
dispatchCatalogState({
type: CatalogActions.SET_MARKETPLACE_CONFIGURATION,
payload: {
marketplaceConfiguration: parser
}
});
}, [dispatchCatalogState]);
return { ...prevValue, marketplaceConfiguration };
});
}, [ setCatalogOptions ]);
const onClubGiftInfoEvent = useCallback((event: ClubGiftInfoEvent) =>
{
const parser = event.getParser();
if(!parser) return;
setCatalogOptions(prevValue =>
{
const clubGifts = parser;
dispatchCatalogState({
type: CatalogActions.SET_CLUB_GIFTS,
payload: {
clubGifts: parser
}
});
}, [dispatchCatalogState]);
return { ...prevValue, clubGifts };
});
}, [ setCatalogOptions ]);
CreateMessageHook(CatalogPagesListEvent, onCatalogPagesListEvent);
CreateMessageHook(CatalogPageMessageEvent, onCatalogPageMessageEvent);

View File

@ -5,6 +5,11 @@
font[size="16"] {
font-size: 20px;
}
.catalog-search-button {
min-width: 30px;
width: 30px;
}
}
.nitro-catalog-navigation-grid-container {

View File

@ -1,10 +1,10 @@
import { FrontPageItem, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetGiftWrappingConfigurationComposer, GetMarketplaceConfigurationMessageComposer, ILinkEventTracker, RoomPreviewer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useReducer, useState } from 'react';
import { FC, useCallback, useEffect, useState } from 'react';
import { AddEventLinkTracker, GetRoomEngine, LocalizeText, RemoveLinkEventTracker } from '../../api';
import { CREDITS, PlaySound } from '../../api/utils/PlaySound';
import { Column } from '../../common/Column';
import { Grid } from '../../common/Grid';
import { CatalogEvent } from '../../events';
import { CatalogPurchasedEvent } from '../../events';
import { BatchUpdates } from '../../hooks';
import { useUiEvent } from '../../hooks/events/ui/ui-event';
import { SendMessageHook } from '../../hooks/messages/message-event';
@ -13,27 +13,25 @@ import { CatalogMessageHandler } from './CatalogMessageHandler';
import { CatalogPage } from './common/CatalogPage';
import { CatalogType } from './common/CatalogType';
import { ICatalogNode } from './common/ICatalogNode';
import { ICatalogOptions } from './common/ICatalogOptions';
import { ICatalogPage } from './common/ICatalogPage';
import { IPageLocalization } from './common/IPageLocalization';
import { IPurchasableOffer } from './common/IPurchasableOffer';
import { IPurchaseOptions } from './common/IPurchaseOptions';
import { RequestedPage } from './common/RequestedPage';
import { SearchResult } from './common/SearchResult';
import { CatalogContextProvider } from './context/CatalogContext';
import { CatalogReducer, initialCatalog } from './reducers/CatalogReducer';
import { CatalogGiftView } from './views/gift/CatalogGiftView';
import { CatalogNavigationView } from './views/navigation/CatalogNavigationView';
import { CatalogPageView } from './views/page/CatalogPageView';
import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout';
import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView';
const DUMMY_PAGE_ID_FOR_OFFER_SEARCH: number = -12345678;
const REQUESTED_PAGE = new RequestedPage();
export const CatalogView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ isBusy, setIsBusy ] = useState(false);
const [ forceRefresh, setForceRefresh ] = useState(false);
const [ pageId, setPageId ] = useState(-1);
const [ previousPageId, setPreviousPageId ] = useState(-1);
const [ currentType, setCurrentType ] = useState(CatalogType.NORMAL);
@ -41,12 +39,12 @@ export const CatalogView: FC<{}> = props =>
const [ offersToNodes, setOffersToNodes ] = useState<Map<number, ICatalogNode[]>>(null);
const [ currentPage, setCurrentPage ] = useState<ICatalogPage>(null);
const [ currentOffer, setCurrentOffer ] = useState<IPurchasableOffer>(null);
const [ purchasableOffer, setPurchasableOffer ] = useState<IPurchasableOffer>(null);
const [ activeNodes, setActiveNodes ] = useState<ICatalogNode[]>([]);
const [ searchResult, setSearchResult ] = useState<SearchResult>(null);
const [ frontPageItems, setFrontPageItems ] = useState<FrontPageItem[]>([]);
const [ roomPreviewer, setRoomPreviewer ] = useState<RoomPreviewer>(null);
const [ catalogState, dispatchCatalogState ] = useReducer(CatalogReducer, initialCatalog);
const [ purchaseOptions, setPurchaseOptions ] = useState<IPurchaseOptions>({});
const [ catalogOptions, setCatalogOptions ] = useState<ICatalogOptions>({});
const resetState = useCallback(() =>
{
@ -58,7 +56,6 @@ export const CatalogView: FC<{}> = props =>
setOffersToNodes(null);
setCurrentPage(null);
setCurrentOffer(null);
setPurchasableOffer(null);
setActiveNodes([]);
setSearchResult(null);
setFrontPageItems([]);
@ -66,13 +63,27 @@ export const CatalogView: FC<{}> = props =>
});
}, []);
const getFirstNodeByName = useCallback((name: string, node: ICatalogNode) =>
const getNodeById = useCallback((id: number, node: ICatalogNode) =>
{
if((node.pageId === id) && (node !== rootNode)) return node;
for(const child of node.children)
{
const found = (getNodeById(id, child) as ICatalogNode);
if(found) return found;
}
return null;
}, [ rootNode ]);
const getNodeByName = useCallback((name: string, node: ICatalogNode) =>
{
if((node.pageName === name) && (node !== rootNode)) return node;
for(const child of node.children)
{
const found = (getFirstNodeByName(name, child) as ICatalogNode);
const found = (getNodeByName(name, child) as ICatalogNode);
if(found) return found;
}
@ -110,43 +121,28 @@ export const CatalogView: FC<{}> = props =>
if(pageId > -1) SendMessageHook(new GetCatalogPageComposer(pageId, offerId, currentType));
}, [ currentType ]);
const selectOffer = useCallback((offerId: number) =>
{
if(!currentPage || !currentPage.offers || offerId < 0) return;
for(const offer of currentPage.offers)
{
if(offer.offerId !== offerId) continue;
setCurrentOffer(offer)
return;
}
}, [ currentPage ]);
const showCatalogPage = useCallback((pageId: number, layoutCode: string, localization: IPageLocalization, offers: IPurchasableOffer[], offerId: number, acceptSeasonCurrencyAsCredits: boolean) =>
{
if(currentPage)
{
if(!forceRefresh && (currentPage.pageId === pageId))
{
if(offerId > -1) selectOffer(offerId);
return;
}
}
const catalogPage = (new CatalogPage(pageId, layoutCode, localization, offers, acceptSeasonCurrencyAsCredits) as ICatalogPage);
BatchUpdates(() =>
{
setCurrentPage(catalogPage);
setPreviousPageId(prevValue => ((pageId > DUMMY_PAGE_ID_FOR_OFFER_SEARCH) ? pageId : prevValue));
setForceRefresh(false);
setPreviousPageId(prevValue => ((pageId !== -1) ? pageId : prevValue));
selectOffer(offerId);
if((offerId > -1) && catalogPage.offers.length)
{
for(const offer of catalogPage.offers)
{
if(offer.offerId !== offerId) continue;
setCurrentOffer(offer)
break;
}
}
});
}, [ currentPage, forceRefresh, selectOffer ]);
}, []);
const activateNode = useCallback((targetNode: ICatalogNode, offerId: number = -1) =>
{
@ -169,7 +165,7 @@ export const CatalogView: FC<{}> = props =>
let node = targetNode;
while(node && node.pageName !== 'root')
while(node && (node.pageName !== 'root'))
{
nodes.push(node);
@ -194,7 +190,9 @@ export const CatalogView: FC<{}> = props =>
{
n.activate();
if(n === targetNode.parent) n.open();
if(n.parent) n.open();
if((n === targetNode.parent) && n.children.length) n.open();
}
if(isActive && isOpen) targetNode.close();
@ -206,6 +204,27 @@ export const CatalogView: FC<{}> = props =>
if(targetNode.pageId > -1) loadCatalogPage(targetNode.pageId, offerId);
}, [ setActiveNodes, loadCatalogPage ]);
const openPageById = useCallback((id: number) =>
{
BatchUpdates(() =>
{
setSearchResult(null);
if(!isVisible)
{
REQUESTED_PAGE.requestById = id;
setIsVisible(true);
}
else
{
const node = getNodeById(id, rootNode);
if(node) activateNode(node);
}
});
}, [ isVisible, rootNode, getNodeById, activateNode ]);
const openPageByName = useCallback((name: string) =>
{
BatchUpdates(() =>
@ -220,12 +239,12 @@ export const CatalogView: FC<{}> = props =>
}
else
{
const node = getFirstNodeByName(name, rootNode);
const node = getNodeByName(name, rootNode);
if(node) activateNode(node);
}
});
}, [ isVisible, rootNode, getFirstNodeByName, activateNode ]);
}, [ isVisible, rootNode, getNodeByName, activateNode ]);
const openPageByOfferId = useCallback((offerId: number) =>
{
@ -250,17 +269,12 @@ export const CatalogView: FC<{}> = props =>
});
}, [ isVisible, getNodesByOfferId, activateNode ]);
const onCatalogEvent = useCallback((event: CatalogEvent) =>
const onCatalogPurchasedEvent = useCallback((event: CatalogPurchasedEvent) =>
{
switch(event.type)
{
case CatalogEvent.PURCHASE_SUCCESS:
PlaySound(CREDITS);
return;
}
PlaySound(CREDITS);
}, []);
useUiEvent(CatalogEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogPurchasedEvent);
const linkReceived = useCallback((url: string) =>
{
@ -294,8 +308,6 @@ export const CatalogView: FC<{}> = props =>
else
{
openPageByName(parts[2]);
return;
}
}
else
@ -367,6 +379,7 @@ export const CatalogView: FC<{}> = props =>
}
return;
case RequestedPage.REQUEST_TYPE_ID:
openPageById(REQUESTED_PAGE.requestById);
REQUESTED_PAGE.resetRequest();
return;
case RequestedPage.REQUEST_TYPE_OFFER:
@ -378,17 +391,20 @@ export const CatalogView: FC<{}> = props =>
REQUESTED_PAGE.resetRequest();
return;
}
}, [ isVisible, rootNode, activeNodes, activateNode, openPageByOfferId, openPageByName ]);
}, [ isVisible, rootNode, activeNodes, activateNode, openPageById, openPageByOfferId, openPageByName ]);
useEffect(() =>
{
if(!currentPage) return;
if(!searchResult && currentPage && (currentPage.pageId === -1)) openPageById(previousPageId);
}, [ searchResult, currentPage, previousPageId, openPageById ]);
setCurrentOffer(null);
useEffect(() =>
{
return () => setCurrentOffer(null);
}, [ currentPage ]);
return (
<CatalogContextProvider value={ { isVisible, isBusy, setIsBusy, pageId, currentType, setCurrentType, rootNode, setRootNode, offersToNodes, setOffersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, purchasableOffer, setPurchasableOffer, activeNodes, setActiveNodes, searchResult, setSearchResult, frontPageItems, setFrontPageItems, roomPreviewer, resetState, loadCatalogPage, showCatalogPage, activateNode, catalogState, dispatchCatalogState } }>
<CatalogContextProvider value={ { isVisible, isBusy, setIsBusy, pageId, currentType, setCurrentType, rootNode, setRootNode, offersToNodes, setOffersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, activeNodes, setActiveNodes, searchResult, setSearchResult, frontPageItems, setFrontPageItems, roomPreviewer, purchaseOptions, setPurchaseOptions, catalogOptions, setCatalogOptions, resetState, getNodesByOfferId, loadCatalogPage, showCatalogPage, activateNode } }>
<CatalogMessageHandler />
{ isVisible &&
<NitroCardView uniqueKey="catalog" className="nitro-catalog">
@ -417,7 +433,7 @@ export const CatalogView: FC<{}> = props =>
<CatalogNavigationView node={ activeNodes[0] } /> }
</Column>
<Column size={ 9 } overflow="hidden">
<CatalogPageView page={ currentPage } roomPreviewer={ roomPreviewer } />
{ GetCatalogLayout(currentPage) }
</Column>
</Grid>
</NitroCardContentView>

View File

@ -12,41 +12,6 @@ export const GetSubscriptionProductIcon = (id: number) =>
return '';
}
export const GetNodeById = (id: number, searchNode: ICatalogNode = null, rootNode: ICatalogNode) =>
{
if(!searchNode) searchNode = rootNode;
if(!searchNode) return null;
if((searchNode.pageId === id) && (searchNode !== rootNode)) return searchNode;
for(const child of searchNode.children)
{
const node = (GetNodeById(id, child, rootNode) as ICatalogNode);
if(node) return node;
}
return null;
}
export const GetNodesByOfferId = (offerId: number, flag: boolean = false, currentOffers: Map<number, ICatalogNode[]>) =>
{
if(!currentOffers || !currentOffers.size) return null;
if(flag)
{
const nodes: ICatalogNode[] = [];
const offers = currentOffers.get(offerId);
if(offers && offers.length) for(const offer of offers) (offer.isVisible && nodes.push(offer));
if(nodes.length) return nodes;
}
return currentOffers.get(offerId);
}
export const GetOfferNodes = (offerNodes: Map<number, ICatalogNode[]>, offerId: number) =>
{
const nodes = offerNodes.get(offerId);

View File

@ -0,0 +1,15 @@
import { ClubGiftInfoParser, ClubOfferData, HabboGroupEntryData, MarketplaceConfigurationMessageParser } from '@nitrots/nitro-renderer';
import { CatalogPetPalette } from './CatalogPetPalette';
import { GiftWrappingConfiguration } from './GiftWrappingConfiguration';
import { SubscriptionInfo } from './SubscriptionInfo';
export interface ICatalogOptions
{
groups?: HabboGroupEntryData[];
petPalettes?: CatalogPetPalette[];
clubOffers?: ClubOfferData[];
clubGifts?: ClubGiftInfoParser;
subscriptionInfo?: SubscriptionInfo;
giftConfiguration?: GiftWrappingConfiguration;
marketplaceConfiguration?: MarketplaceConfigurationMessageParser;
}

View File

@ -4,13 +4,13 @@ import { IPurchasableOffer } from './IPurchasableOffer';
export interface IProduct
{
getIconUrl(offer?: IPurchasableOffer): string;
readonly productType: string;
readonly productClassId: number;
readonly extraParam: string;
readonly productCount: number;
readonly productData: IProductData;
readonly furnitureData: IFurnitureData;
readonly isUniqueLimitedItem: boolean;
readonly uniqueLimitedItemSeriesSize: number;
productType: string;
productClassId: number;
extraParam: string;
productCount: number;
productData: IProductData;
furnitureData: IFurnitureData;
isUniqueLimitedItem: boolean;
uniqueLimitedItemSeriesSize: number;
uniqueLimitedItemsLeft: number;
}

View File

@ -3,21 +3,21 @@ import { IProduct } from './IProduct';
export interface IPurchasableOffer
{
readonly clubLevel: number;
clubLevel: number;
page: ICatalogPage;
readonly offerId: number;
readonly localizationId: string;
readonly priceInCredits: number;
readonly priceInActivityPoints: number;
readonly activityPointType: number;
readonly giftable: boolean;
readonly product: IProduct;
readonly pricingModel: string;
readonly priceType: string;
readonly bundlePurchaseAllowed: boolean;
readonly isRentOffer: boolean;
readonly badgeCode: string;
readonly localizationName: string;
readonly localizationDescription: string;
readonly products: IProduct[];
offerId: number;
localizationId: string;
priceInCredits: number;
priceInActivityPoints: number;
activityPointType: number;
giftable: boolean;
product: IProduct;
pricingModel: string;
priceType: string;
bundlePurchaseAllowed: boolean;
isRentOffer: boolean;
badgeCode: string;
localizationName: string;
localizationDescription: string;
products: IProduct[];
}

View File

@ -0,0 +1,10 @@
import { IObjectData } from '@nitrots/nitro-renderer';
export interface IPurchaseOptions
{
quantity?: number;
extraData?: string;
extraParamRequired?: boolean;
purchaseCallback?: Function;
previewStuffData?: IObjectData;
}

View File

@ -1,13 +1,14 @@
import { FrontPageItem, RoomPreviewer } from '@nitrots/nitro-renderer';
import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react';
import { ICatalogNode } from '../common/ICatalogNode';
import { ICatalogOptions } from '../common/ICatalogOptions';
import { ICatalogPage } from '../common/ICatalogPage';
import { IPageLocalization } from '../common/IPageLocalization';
import { IPurchasableOffer } from '../common/IPurchasableOffer';
import { IPurchaseOptions } from '../common/IPurchaseOptions';
import { SearchResult } from '../common/SearchResult';
import { ICatalogAction, ICatalogState } from '../reducers/CatalogReducer';
export interface ICatalogContext
interface ICatalogContext
{
isVisible: boolean;
isBusy: boolean;
@ -23,8 +24,6 @@ export interface ICatalogContext
setCurrentPage: Dispatch<SetStateAction<ICatalogPage>>;
currentOffer: IPurchasableOffer;
setCurrentOffer: Dispatch<SetStateAction<IPurchasableOffer>>;
purchasableOffer: IPurchasableOffer;
setPurchasableOffer: Dispatch<SetStateAction<IPurchasableOffer>>;
activeNodes: ICatalogNode[];
setActiveNodes: Dispatch<SetStateAction<ICatalogNode[]>>;
searchResult: SearchResult;
@ -32,13 +31,15 @@ export interface ICatalogContext
frontPageItems: FrontPageItem[];
setFrontPageItems: Dispatch<SetStateAction<FrontPageItem[]>>;
roomPreviewer: RoomPreviewer;
purchaseOptions: IPurchaseOptions;
setPurchaseOptions: Dispatch<SetStateAction<IPurchaseOptions>>;
catalogOptions: ICatalogOptions;
setCatalogOptions: Dispatch<SetStateAction<ICatalogOptions>>;
resetState: () => void;
getNodesByOfferId: (offerId: number, flag?: boolean) => ICatalogNode[];
loadCatalogPage: (pageId: number, offerId: number) => void;
showCatalogPage: (pageId: number, layoutCode: string, localization: IPageLocalization, offers: IPurchasableOffer[], offerId: number, acceptSeasonCurrencyAsCredits: boolean) => void;
activateNode: (targetNode: ICatalogNode) => void;
catalogState: ICatalogState;
dispatchCatalogState: Dispatch<ICatalogAction>;
}
const CatalogContext = createContext<ICatalogContext>({
@ -56,8 +57,6 @@ const CatalogContext = createContext<ICatalogContext>({
setCurrentPage: null,
currentOffer: null,
setCurrentOffer: null,
purchasableOffer: null,
setPurchasableOffer: null,
activeNodes: null,
setActiveNodes: null,
searchResult: null,
@ -65,12 +64,15 @@ const CatalogContext = createContext<ICatalogContext>({
frontPageItems: null,
setFrontPageItems: null,
roomPreviewer: null,
purchaseOptions: null,
setPurchaseOptions: null,
catalogOptions: null,
setCatalogOptions: null,
resetState: null,
getNodesByOfferId: null,
loadCatalogPage: null,
showCatalogPage: null,
activateNode: null,
catalogState: null,
dispatchCatalogState: null
activateNode: null
});
export const CatalogContextProvider: FC<ProviderProps<ICatalogContext>> = props =>

View File

@ -1,117 +0,0 @@
import { ClubGiftInfoParser, ClubOfferData, GiftWrappingConfigurationParser, MarketplaceConfigurationMessageParser } from '@nitrots/nitro-renderer';
import { HabboGroupEntryData } from '@nitrots/nitro-renderer/src/nitro/communication/messages/parser/user/HabboGroupEntryData';
import { Reducer } from 'react';
import { CatalogPetPalette } from '../common/CatalogPetPalette';
import { GiftWrappingConfiguration } from '../common/GiftWrappingConfiguration';
import { SubscriptionInfo } from '../common/SubscriptionInfo';
export interface ICatalogState
{
groups: HabboGroupEntryData[];
petPalettes: CatalogPetPalette[];
clubOffers: ClubOfferData[];
clubGifts: ClubGiftInfoParser;
subscriptionInfo: SubscriptionInfo;
giftConfiguration: GiftWrappingConfiguration;
marketplaceConfiguration: MarketplaceConfigurationMessageParser;
}
export interface ICatalogAction
{
type: string;
payload: {
groups?: HabboGroupEntryData[];
petPalette?: CatalogPetPalette;
clubOffers?: ClubOfferData[];
clubGifts?: ClubGiftInfoParser;
subscriptionInfo?: SubscriptionInfo;
giftConfiguration?: GiftWrappingConfigurationParser;
marketplaceConfiguration?: MarketplaceConfigurationMessageParser;
}
}
export class CatalogActions
{
public static RESET_STATE: string = 'CA_RESET_STATE';
public static SET_CLUB_OFFERS: string = 'CA_SET_CLUB_OFFERS';
public static SET_GROUPS: string = 'CA_SET_GROUPS';
public static SET_PET_PALETTE: string = 'CA_SET_PET_PALETTE';
public static SET_SEARCH_RESULT: string = 'CA_SET_SEARCH_RESULT';
public static SET_SUBSCRIPTION_INFO: string = 'CA_SET_SUBSCRIPTION_INFO';
public static SET_GIFT_CONFIGURATION: string = 'CA_SET_GIFT_CONFIGURATION';
public static SET_CLUB_GIFTS: string = 'CA_SET_CLUB_GIFTS';
public static SET_MARKETPLACE_CONFIGURATION: string = 'CA_SET_MARKETPLACE_CONFIGURATION';
}
export const initialCatalog: ICatalogState = {
groups: [],
petPalettes: [],
clubOffers: null,
clubGifts: null,
subscriptionInfo: new SubscriptionInfo(),
giftConfiguration: null,
marketplaceConfiguration: null
}
export const CatalogReducer: Reducer<ICatalogState, ICatalogAction> = (state, action) =>
{
switch(action.type)
{
case CatalogActions.SET_GROUPS: {
const groups = (action.payload.groups || null);
return { ...state, groups };
}
case CatalogActions.SET_PET_PALETTE: {
const petPalette = (action.payload.petPalette || null);
let petPalettes = [ ...state.petPalettes ];
for(let i = 0; i < petPalettes.length; i++)
{
const palette = petPalettes[i];
if(palette.breed === petPalette.breed)
{
petPalettes.splice(i, 1);
break;
}
}
petPalettes.push(petPalette);
return { ...state, petPalettes };
}
case CatalogActions.SET_CLUB_OFFERS: {
const clubOffers = (action.payload.clubOffers || null);
return { ...state, clubOffers };
}
case CatalogActions.SET_SUBSCRIPTION_INFO: {
const subscriptionInfo = (action.payload.subscriptionInfo || null);
return { ...state, subscriptionInfo };
}
case CatalogActions.RESET_STATE: {
return { ...initialCatalog };
}
case CatalogActions.SET_GIFT_CONFIGURATION: {
const giftConfiguration = new GiftWrappingConfiguration((action.payload.giftConfiguration || null));
return { ...state, giftConfiguration };
}
case CatalogActions.SET_CLUB_GIFTS: {
const clubGifts = (action.payload.clubGifts || state.clubGifts || null);
return { ...state, clubGifts };
}
case CatalogActions.SET_MARKETPLACE_CONFIGURATION: {
const marketplaceConfiguration = (action.payload.marketplaceConfiguration || state.marketplaceConfiguration || null);
return { ...state, marketplaceConfiguration }
}
default:
return state;
}
}

View File

@ -1,26 +1,16 @@
import { NitroToolbarAnimateIconEvent, TextureUtils, ToolbarIconEnum } from '@nitrots/nitro-renderer';
import { FC, useCallback, useRef, useState } from 'react';
import { FC, useCallback, useRef } from 'react';
import { GetRoomEngine } from '../../../../api';
import { CatalogEvent, CatalogSelectProductEvent } from '../../../../events';
import { CatalogWidgetEvent } from '../../../../events/catalog/CatalogWidgetEvent';
import { CatalogPurchasedEvent } from '../../../../events';
import { useUiEvent } from '../../../../hooks';
import { RoomPreviewerView } from '../../../../views/shared/room-previewer/RoomPreviewerView';
import { RoomPreviewerViewProps } from '../../../../views/shared/room-previewer/RoomPreviewerView.types';
import { IPurchasableOffer } from '../../common/IPurchasableOffer';
export const CatalogRoomPreviewerView: FC<RoomPreviewerViewProps> = props =>
{
const { roomPreviewer = null } = props;
const [ offer, setOffer ] = useState<IPurchasableOffer>(null);
const elementRef = useRef<HTMLDivElement>(null);
const onCatalogSelectProductEvent = useCallback((event: CatalogSelectProductEvent) =>
{
setOffer(event.offer);
}, []);
useUiEvent(CatalogWidgetEvent.SELECT_PRODUCT, onCatalogSelectProductEvent)
const animatePurchase = useCallback(() =>
{
if(!elementRef) return;
@ -45,17 +35,12 @@ export const CatalogRoomPreviewerView: FC<RoomPreviewerViewProps> = props =>
GetRoomEngine().events.dispatchEvent(event);
}, [ roomPreviewer ]);
const onCatalogEvent = useCallback((event: CatalogEvent) =>
const onCatalogPurchasedEvent = useCallback((event: CatalogPurchasedEvent) =>
{
switch(event.type)
{
case CatalogEvent.PURCHASE_SUCCESS:
animatePurchase();
return;
}
animatePurchase();
}, [ animatePurchase ]);
useUiEvent(CatalogEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogPurchasedEvent);
return (
<div ref={ elementRef }>

View File

@ -9,7 +9,7 @@ import { Column } from '../../../../common/Column';
import { Flex } from '../../../../common/Flex';
import { FormGroup } from '../../../../common/FormGroup';
import { Text } from '../../../../common/Text';
import { CatalogEvent } from '../../../../events';
import { CatalogEvent, CatalogPurchasedEvent } from '../../../../events';
import { CatalogInitGiftEvent } from '../../../../events/catalog/CatalogInitGiftEvent';
import { BatchUpdates, SendMessageHook, useUiEvent } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutGiftCardView } from '../../../../layout';
@ -32,8 +32,8 @@ export const CatalogGiftView: FC<{}> = props =>
const [ maxBoxIndex, setMaxBoxIndex ] = useState<number>(0);
const [ maxRibbonIndex, setMaxRibbonIndex ] = useState<number>(0);
const [ receiverNotFound, setReceiverNotFound ] = useState<boolean>(false);
const { catalogState = null } = useCatalogContext();
const { giftConfiguration = null } = catalogState;
const { catalogOptions = null } = useCatalogContext();
const { giftConfiguration = null } = catalogOptions;
const close = useCallback(() =>
{
@ -56,7 +56,7 @@ export const CatalogGiftView: FC<{}> = props =>
{
switch(event.type)
{
case CatalogEvent.PURCHASE_SUCCESS:
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
close();
return;
case CatalogEvent.INIT_GIFT:
@ -77,7 +77,7 @@ export const CatalogGiftView: FC<{}> = props =>
}
}, [ close ]);
useUiEvent(CatalogEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogEvent.INIT_GIFT, onCatalogEvent);
useUiEvent(CatalogEvent.GIFT_RECEIVER_NOT_FOUND, onCatalogEvent);

View File

@ -19,7 +19,7 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
return (
<>
<LayoutGridItem column={ false } itemActive={ node.isActive } onClick={ event => activateNode(node) }>
<LayoutGridItem gap={ 1 } column={ false } itemActive={ node.isActive } onClick={ event => activateNode(node) }>
<CatalogIconView icon={ node.iconId } />
<Text grow truncate>{ node.localization }</Text>
{ node.isBranch &&

View File

@ -1,121 +0,0 @@
import { IObjectData, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect } from 'react';
import { GetAvatarRenderManager, GetSessionDataManager } from '../../../../api';
import { CatalogPageReadyEvent, SetRoomPreviewerStuffDataEvent } from '../../../../events';
import { dispatchUiEvent, useUiEvent } from '../../../../hooks';
import { FurniCategory } from '../../common/FurniCategory';
import { ICatalogPage } from '../../common/ICatalogPage';
import { IPurchasableOffer } from '../../common/IPurchasableOffer';
import { ProductTypeEnum } from '../../common/ProductTypeEnum';
import { useCatalogContext } from '../../context/CatalogContext';
import { GetCatalogLayout } from './layout/GetCatalogLayout';
export interface CatalogPageViewProps
{
page: ICatalogPage;
roomPreviewer: RoomPreviewer;
}
export const CatalogPageView: FC<CatalogPageViewProps> = props =>
{
const { page = null, roomPreviewer = null } = props;
const { currentOffer = null } = useCatalogContext();
const updatePreviewerForOffer = useCallback((offer: IPurchasableOffer, stuffData: IObjectData = null) =>
{
if(!offer || !roomPreviewer) return;
const product = offer.product;
if(!product && !product.furnitureData && (product.productType !== ProductTypeEnum.ROBOT)) return;
switch(product.productType.toLowerCase())
{
case ProductTypeEnum.ROBOT: {
roomPreviewer.updateObjectRoom('default', 'default', 'default');
const figure = GetAvatarRenderManager().getFigureStringWithFigureIds(product.extraParam, 'm', []);
roomPreviewer.addAvatarIntoRoom(figure, 0);
return;
}
case ProductTypeEnum.FLOOR: {
roomPreviewer.updateObjectRoom('default', 'default', 'default');
if(product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET)
{
const setIds: number[] = [];
const sets = product.furnitureData.customParams.split(',');
for(const set of sets)
{
const setId = parseInt(set);
if(GetAvatarRenderManager().isValidFigureSetForGender(setId, GetSessionDataManager().gender)) setIds.push(setId);
}
const figure = GetAvatarRenderManager().getFigureStringWithFigureIds(GetSessionDataManager().figure, GetSessionDataManager().gender, setIds);
roomPreviewer.addAvatarIntoRoom(figure, 0);
}
else
{
roomPreviewer.addFurnitureIntoRoom(product.productClassId, new Vector3d(90), stuffData);
}
return;
}
case ProductTypeEnum.WALL: {
switch(product.furnitureData.className)
{
case 'floor':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(product.extraParam);
break;
case 'wallpaper':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(null, product.extraParam);
break;
case 'landscape':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(null, null, product.extraParam);
break;
default:
roomPreviewer.updateObjectRoom('default', 'default', 'default');
roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam);
return;
}
const windowData = GetSessionDataManager().getWallItemDataByName('noob_window_double');
if(windowData) roomPreviewer.addWallItemIntoRoom(windowData.id, new Vector3d(90, 0, 0), windowData.customParams);
return;
}
}
}, [ roomPreviewer ]);
const onSetRoomPreviewerStuffDataEvent = useCallback((event: SetRoomPreviewerStuffDataEvent) =>
{
if(roomPreviewer) roomPreviewer.reset(false);
updatePreviewerForOffer(event.offer, event.stuffData);
}, [ roomPreviewer, updatePreviewerForOffer ]);
useUiEvent(SetRoomPreviewerStuffDataEvent.UPDATE_STUFF_DATA, onSetRoomPreviewerStuffDataEvent);
useEffect(() =>
{
if(!currentOffer) return;
updatePreviewerForOffer(currentOffer);
}, [ currentOffer, updatePreviewerForOffer ]);
useEffect(() =>
{
dispatchUiEvent(new CatalogPageReadyEvent());
}, [ page ]);
if(!page) return null;
return GetCatalogLayout(page, roomPreviewer);
}

View File

@ -1,8 +1,6 @@
import { RoomPreviewer } from '@nitrots/nitro-renderer';
import { ICatalogPage } from '../../../common/ICatalogPage';
export interface CatalogLayoutProps
{
page: ICatalogPage;
roomPreviewer: RoomPreviewer;
}

View File

@ -1,78 +1,58 @@
import { StringDataType } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { Base } from '../../../../../common/Base';
import { Column } from '../../../../../common/Column';
import { Flex } from '../../../../../common/Flex';
import { Grid } from '../../../../../common/Grid';
import { LayoutGridItem } from '../../../../../common/layout/LayoutGridItem';
import { Text } from '../../../../../common/Text';
import { InventoryBadgesUpdatedEvent, SetRoomPreviewerStuffDataEvent } from '../../../../../events';
import { InventoryBadgesRequestEvent } from '../../../../../events/inventory/InventoryBadgesRequestEvent';
import { dispatchUiEvent, useUiEvent } from '../../../../../hooks';
import { BadgeImageView } from '../../../../../views/shared/badge-image/BadgeImageView';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogProductPreviewView } from '../offers/CatalogPageOfferPreviewView';
import { CatalogPageOffersView } from '../offers/CatalogPageOffersView';
import { CatalogBadgeSelectorWidgetView } from '../widgets/CatalogBadgeSelectorWidgetView';
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutBadgeDisplayView: FC<CatalogLayoutProps> = props =>
{
const { page = null, roomPreviewer = null } = props;
const [ badges, setBadges ] = useState<string[]>([]);
const [ currentBadge, setCurrentBadge ] = useState<string>(null);
const { page = null } = props;
const { currentOffer = null } = useCatalogContext();
const onInventoryBadgesUpdatedEvent = useCallback((event: InventoryBadgesUpdatedEvent) =>
{
setBadges(event.badges);
}, []);
useUiEvent(InventoryBadgesUpdatedEvent.BADGES_UPDATED, onInventoryBadgesUpdatedEvent);
useEffect(() =>
{
dispatchUiEvent(new InventoryBadgesRequestEvent(InventoryBadgesRequestEvent.REQUEST_BADGES));
}, []);
useEffect(() =>
{
if(!currentBadge || !currentOffer) return;
const productData = [];
productData.push('0');
productData.push(currentBadge);
productData.push('');
productData.push('');
productData.push('');
const stringDataType = new StringDataType();
stringDataType.setValue(productData);
dispatchUiEvent(new SetRoomPreviewerStuffDataEvent(currentOffer, stringDataType));
}, [ currentBadge, currentOffer, roomPreviewer ]);
return (
<Grid>
<Column size={ 7 } overflow="hidden">
<CatalogPageOffersView shrink offers={ page.offers } />
<Column gap={ 1 } overflow="hidden">
<Text truncate shrink fontWeight="bold">{ LocalizeText('catalog_selectbadge') }</Text>
<Grid grow columnCount={ 5 } overflow="auto">
{ badges && (badges.length > 0) && badges.map(code =>
{
return (
<LayoutGridItem key={ code } itemActive={ (currentBadge === code) } onMouseDown={ event => setCurrentBadge(code) }>
<BadgeImageView badgeCode={ code } />
</LayoutGridItem>
);
}) }
</Grid>
<>
<CatalogFirstProductSelectorWidgetView />
<Grid>
<Column size={ 7 } overflow="hidden">
<CatalogItemGridWidgetView shrink />
<Column gap={ 1 } overflow="hidden">
<Text truncate shrink fontWeight="bold">{ LocalizeText('catalog_selectbadge') }</Text>
<CatalogBadgeSelectorWidgetView />
</Column>
</Column>
</Column>
<Column size={ 5 } overflow="hidden">
{ !!currentOffer &&
<CatalogProductPreviewView offer={ currentOffer } roomPreviewer={ roomPreviewer } extra={ currentBadge } disabled={ !currentBadge } /> }
</Column>
</Grid>
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Base position="relative" overflow="hidden">
<CatalogViewProductWidgetView />
<CatalogLimitedItemWidgetView fullWidth position="absolute" className="top-1" />
</Base>
<Column grow gap={ 1 }>
<Text grow truncate>{ currentOffer.localizationName }</Text>
<Flex justifyContent="end">
<CatalogTotalPriceWidget alignItems="end" />
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</>
);
}

View File

@ -1,15 +1,22 @@
import { FC } from 'react';
import { Base } from '../../../../../common/Base';
import { Column } from '../../../../../common/Column';
import { Flex } from '../../../../../common/Flex';
import { Grid } from '../../../../../common/Grid';
import { Text } from '../../../../../common/Text';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogPageDetailsView } from '../../page-details/CatalogPageDetailsView';
import { CatalogProductPreviewView } from '../offers/CatalogPageOfferPreviewView';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogSpinnerWidgetView } from '../widgets/CatalogSpinnerWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
{
const { page = null, roomPreviewer = null } = props;
const { page = null } = props;
const { currentOffer = null } = useCatalogContext();
return (
@ -17,11 +24,30 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
<Column size={ 7 } overflow="hidden">
<CatalogItemGridWidgetView />
</Column>
<Column size={ 5 } overflow="hidden">
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<CatalogPageDetailsView page={ page } /> }
{ !!currentOffer &&
<CatalogProductPreviewView offer={ currentOffer } roomPreviewer={ roomPreviewer } /> }
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Base position="relative" overflow="hidden">
<CatalogViewProductWidgetView />
<CatalogLimitedItemWidgetView fullWidth position="absolute" className="top-1" />
<CatalogAddOnBadgeWidgetView position="absolute" className="bg-muted rounded bottom-1 end-1" />
</Base>
<Column grow gap={ 1 }>
<Text grow truncate>{ currentOffer.localizationName }</Text>
<Flex justifyContent="between">
<Column gap={ 1 }>
<CatalogSpinnerWidgetView />
</Column>
<CatalogTotalPriceWidget justifyContent="end" alignItems="end" />
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
);

View File

@ -1,67 +1,51 @@
import { CatalogGroupsComposer, StringDataType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { FC } from 'react';
import { Base } from '../../../../../common/Base';
import { Column } from '../../../../../common/Column';
import { Flex } from '../../../../../common/Flex';
import { Grid } from '../../../../../common/Grid';
import { SetRoomPreviewerStuffDataEvent } from '../../../../../events';
import { dispatchUiEvent } from '../../../../../hooks';
import { SendMessageHook } from '../../../../../hooks/messages';
import { Text } from '../../../../../common/Text';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogSelectGroupView } from '../../select-group/CatalogSelectGroupView';
import { CatalogProductPreviewView } from '../offers/CatalogPageOfferPreviewView';
import { CatalogPageOffersView } from '../offers/CatalogPageOffersView';
import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView';
import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayouGuildCustomFurniView: FC<CatalogLayoutProps> = props =>
{
const { page = null, roomPreviewer = null } = props;
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
const { currentOffer = null, catalogState = null } = useCatalogContext();
const { groups = null } = catalogState;
const selectedGroup = useMemo(() =>
{
if(!groups || !groups.length) return;
return groups[selectedGroupIndex];
}, [ groups, selectedGroupIndex ]);
useEffect(() =>
{
if(!page) return;
SendMessageHook(new CatalogGroupsComposer());
}, [ page ]);
useEffect(() =>
{
if(!currentOffer || !groups[selectedGroupIndex]) return;
const productData = [];
productData.push('0');
productData.push(groups[selectedGroupIndex].groupId);
productData.push(groups[selectedGroupIndex].badgeCode);
productData.push(groups[selectedGroupIndex].colorA);
productData.push(groups[selectedGroupIndex].colorB);
const stringDataType = new StringDataType();
stringDataType.setValue(productData);
dispatchUiEvent(new SetRoomPreviewerStuffDataEvent(currentOffer, stringDataType));
}, [ groups, currentOffer, selectedGroupIndex ]);
if(!groups) return null;
const { page = null } = props;
const { currentOffer = null } = useCatalogContext();
return (
<Grid>
<Column size={ 7 } overflow="hidden">
<CatalogPageOffersView offers={ page.offers } />
<CatalogItemGridWidgetView />
</Column>
<Column size={ 5 } overflow="hidden">
{ !!currentOffer &&
<CatalogProductPreviewView offer={ currentOffer } roomPreviewer={ roomPreviewer } badgeCode={ ((selectedGroup && selectedGroup.badgeCode) || null) } extra={ groups[selectedGroupIndex] ? groups[selectedGroupIndex].groupId.toString() : '' } disabled={ !(!!groups[selectedGroupIndex]) }>
<CatalogSelectGroupView selectedGroupIndex={ selectedGroupIndex } setSelectedGroupIndex={ setSelectedGroupIndex } />
</CatalogProductPreviewView> }
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Base position="relative" overflow="hidden">
<CatalogViewProductWidgetView />
<CatalogGuildBadgeWidgetView position="absolute" className="bottom-1 end-1" />
</Base>
<Column grow gap={ 1 }>
<Text truncate>{ currentOffer.localizationName }</Text>
<Base grow>
<CatalogGuildSelectorWidgetView />
</Base>
<Flex justifyContent="end">
<CatalogTotalPriceWidget alignItems="end" />
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
);

View File

@ -5,7 +5,6 @@ import { Column } from '../../../../../common/Column';
import { Grid } from '../../../../../common/Grid';
import { SendMessageHook } from '../../../../../hooks/messages';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogSelectGroupView } from '../../select-group/CatalogSelectGroupView';
import { CatalogProductPreviewView } from '../offers/CatalogPageOfferPreviewView';
import { CatalogLayoutProps } from './CatalogLayout.types';
@ -13,8 +12,8 @@ export const CatalogLayouGuildForumView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
const { currentOffer = null, setCurrentOffer = null, catalogState = null, dispatchCatalogState = null } = useCatalogContext();
const { groups = null } = catalogState;
const { currentOffer = null, setCurrentOffer = null, catalogOptions = null } = useCatalogContext();
const { groups = null } = catalogOptions;
useEffect(() =>
{
@ -31,7 +30,7 @@ export const CatalogLayouGuildForumView: FC<CatalogLayoutProps> = props =>
<Column size={ 5 } overflow="hidden">
{ !!currentOffer &&
<CatalogProductPreviewView offer={ currentOffer } roomPreviewer={ null } extra={ groups[selectedGroupIndex] ? groups[selectedGroupIndex].groupId.toString() : '' } disabled={ !(!!groups[selectedGroupIndex]) }>
<CatalogSelectGroupView selectedGroupIndex={ selectedGroupIndex } setSelectedGroupIndex={ setSelectedGroupIndex } />
{/* <CatalogSelectGroupView selectedGroupIndex={ selectedGroupIndex } setSelectedGroupIndex={ setSelectedGroupIndex } /> */}
</CatalogProductPreviewView> }
</Column>
</Grid>

View File

@ -1,27 +1,43 @@
import { FC } from 'react';
import { Column } from '../../../../../common/Column';
import { Grid } from '../../../../../common/Grid';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogPageDetailsView } from '../../page-details/CatalogPageDetailsView';
import { CatalogPurchaseView } from '../purchase/CatalogPurchaseView';
import { Text } from '../../../../../common/Text';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogBundleGridWidgetView } from '../widgets/CatalogBundleGridWidgetView';
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogSimplePriceWidgetView } from '../widgets/CatalogSimplePriceWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutRoomBundleView: FC<CatalogLayoutProps> = props =>
{
const { page = null, roomPreviewer = null } = props;
const { currentOffer = null } = useCatalogContext();
const { page = null } = props;
return (
<Grid>
<Column size={ 7 } overflow="hidden">
<Grid grow overflow="auto">
{/* { currentOffer && currentOffer.products && (activeOffer.products.length > 0) && activeOffer.products.map((product, index) => <CatalogProductView key={ index } itemActive={ false } product={ product } />)} */}
</Grid>
</Column>
<Column size={ 5 } overflow="hidden">
<CatalogPageDetailsView page={ page } />
{ currentOffer && <CatalogPurchaseView offer={ currentOffer } pageId={ page.pageId } /> }
</Column>
</Grid>
<>
<CatalogFirstProductSelectorWidgetView />
<Grid>
<Column size={ 7 } overflow="hidden">
{ !!page.localization.getText(2) &&
<Text dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } /> }
<Column grow overflow="hidden" className="bg-muted p-2 rounded">
<CatalogBundleGridWidgetView fullWidth className="nitro-catalog-layout-bundle-grid" />
</Column>
</Column>
<Column size={ 5 } overflow="hidden" gap={ 1 }>
{ !!page.localization.getText(1) &&
<Text center small overflow="auto">{ page.localization.getText(1) }</Text> }
<Column grow position="relative" overflow="hidden" gap={ 0 }>
{ !!page.localization.getImage(1) &&
<img alt="" className="flex-grow-1" src={ page.localization.getImage(1) } /> }
<CatalogAddOnBadgeWidgetView position="absolute" className="bg-muted rounded bottom-0 start-0" />
<CatalogSimplePriceWidgetView position="absolute" className="bottom-0 end-0" />
</Column>
<Column gap={ 1 }>
<CatalogPurchaseWidgetView />
</Column>
</Column>
</Grid>
</>
);
}

View File

@ -1,10 +1,10 @@
import { FC } from 'react';
import { Column } from '../../../../../common/Column';
import { Flex } from '../../../../../common/Flex';
import { Grid } from '../../../../../common/Grid';
import { Text } from '../../../../../common/Text';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogBundleGridWidgetView } from '../widgets/CatalogBundleGridWidgetView';
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogSimplePriceWidgetView } from '../widgets/CatalogSimplePriceWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
@ -13,27 +13,27 @@ export const CatalogLayoutSingleBundleView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const imageUrl = page.localization.getImage(1);
return (
<>
<CatalogFirstProductSelectorWidgetView />
<Grid>
<Column justifyContent="between" size={ 7 } overflow="hidden">
<Text dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } />
<Column fit overflow="hidden" className="bg-muted p-2 rounded">
<CatalogBundleGridWidgetView className="nitro-catalog-layout-bundle-grid" fullWidth />
<Column size={ 7 } overflow="hidden">
{ !!page.localization.getText(2) &&
<Text dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } /> }
<Column grow overflow="hidden" className="bg-muted p-2 rounded">
<CatalogBundleGridWidgetView fullWidth className="nitro-catalog-layout-bundle-grid" />
</Column>
</Column>
<Column size={ 5 } overflow="hidden" gap={ 1 }>
<Text center overflow="auto">{ page.localization.getText(1) }</Text>
<Flex fullHeight center position="relative" overflow="hidden">
{ imageUrl && <img className="" alt="" src={ imageUrl } /> }
<CatalogAddOnBadgeWidgetView position="absolute" className="bottom-0 start-0" />
</Flex>
<Column gap={ 1 } position="relative">
<Flex center>
<CatalogSimplePriceWidgetView />
</Flex>
{ !!page.localization.getText(1) &&
<Text center small overflow="auto">{ page.localization.getText(1) }</Text> }
<Column grow position="relative" overflow="hidden" gap={ 0 }>
{ !!page.localization.getImage(1) &&
<img alt="" className="flex-grow-1" src={ page.localization.getImage(1) } /> }
<CatalogAddOnBadgeWidgetView position="absolute" className="bg-muted rounded bottom-0 start-0" />
<CatalogSimplePriceWidgetView position="absolute" className="bottom-0 end-0" />
</Column>
<Column gap={ 1 }>
<CatalogPurchaseWidgetView />
</Column>
</Column>

View File

@ -1,73 +1,51 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText } from '../../../../../api';
import { Button } from '../../../../../common/Button';
import { ButtonGroup } from '../../../../../common/ButtonGroup';
import { NitroPoint } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react';
import { Base } from '../../../../../common/Base';
import { Column } from '../../../../../common/Column';
import { Flex } from '../../../../../common/Flex';
import { Grid } from '../../../../../common/Grid';
import { BatchUpdates } from '../../../../../hooks';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { Text } from '../../../../../common/Text';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogProductPreviewView } from '../offers/CatalogPageOfferPreviewView';
import { CatalogPageOffersView } from '../offers/CatalogPageOffersView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogSpacesWidgetView } from '../widgets/CatalogSpacesWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutSpacesView: FC<CatalogLayoutProps> = props =>
{
const { page = null, roomPreviewer = null } = props;
const [ groups, setGroups ] = useState<IPurchasableOffer[][]>([]);
const [ activeGroupIndex, setActiveGroupIndex ] = useState(-1);
const { currentOffer = null, catalogState } = useCatalogContext();
const groupNames = [ 'floors', 'walls', 'views' ];
const { page = null } = props;
const { currentOffer = null, roomPreviewer = null } = useCatalogContext();
useEffect(() =>
{
if(!page) return;
const groupedOffers: IPurchasableOffer[][] = [ [], [], [] ];
for(const offer of page.offers)
{
const product = offer.product
if(!product) continue;
if(!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;
}
}
BatchUpdates(() =>
{
setGroups(groupedOffers);
setActiveGroupIndex(0);
});
}, [ page ]);
roomPreviewer.updatePreviewObjectBoundingRectangle(new NitroPoint());
}, [ roomPreviewer ]);
return (
<Grid>
<Column size={ 7 } overflow="hidden">
<ButtonGroup>
{ groupNames.map((name, index) => <Button key={ index } active={ (activeGroupIndex === index) } onClick={ event => setActiveGroupIndex(index) }>{ LocalizeText(`catalog.spaces.tab.${ name }`) }</Button>)}
</ButtonGroup>
<CatalogPageOffersView offers={ groups[activeGroupIndex] } />
<CatalogSpacesWidgetView />
</Column>
<Column size={ 5 } overflow="hidden">
{ !!currentOffer &&
<CatalogProductPreviewView offer={ currentOffer } roomPreviewer={ roomPreviewer } /> }
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Base position="relative" overflow="hidden">
<CatalogViewProductWidgetView />
</Base>
<Column grow gap={ 1 }>
<Text grow truncate>{ currentOffer.localizationName }</Text>
<Flex justifyContent="end">
<CatalogTotalPriceWidget alignItems="end" />
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
);

View File

@ -1,26 +1,56 @@
import { FC, useState } from 'react';
import { FC, useEffect, useState } from 'react';
import { Column } from '../../../../../common/Column';
import { Flex } from '../../../../../common/Flex';
import { Grid } from '../../../../../common/Grid';
import { Text } from '../../../../../common/Text';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogProductPreviewView } from '../offers/CatalogPageOfferPreviewView';
import { CatalogPageOffersView } from '../offers/CatalogPageOffersView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
{
const { page = null, roomPreviewer = null } = props;
const { page = null } = props;
const [ trophyText, setTrophyText ] = useState<string>('');
const { currentOffer = null } = useCatalogContext();
const { currentOffer = null, setPurchaseOptions = null } = useCatalogContext();
useEffect(() =>
{
if(!currentOffer) return;
setPurchaseOptions(prevValue =>
{
const extraData = trophyText;
return { ...prevValue, extraData };
});
}, [ currentOffer, trophyText, setPurchaseOptions ]);
return (
<Grid>
<Column size={ 7 } overflow="hidden">
<CatalogPageOffersView offers={ page.offers } />
<CatalogItemGridWidgetView />
<textarea className="flex-grow-1 form-control w-100" defaultValue={ trophyText || '' } onChange={ event => setTrophyText(event.target.value) } />
</Column>
<Column size={ 5 } overflow="hidden">
{ !!currentOffer &&
<CatalogProductPreviewView offer={ currentOffer } roomPreviewer={ roomPreviewer } extra={ trophyText } /> }
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<CatalogViewProductWidgetView />
<Column grow gap={ 1 }>
<Text grow truncate>{ currentOffer.localizationName }</Text>
<Flex justifyContent="end">
<CatalogTotalPriceWidget alignItems="end" />
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
);

View File

@ -7,6 +7,7 @@ import { Flex } from '../../../../../common/Flex';
import { Grid } from '../../../../../common/Grid';
import { LayoutGridItem } from '../../../../../common/layout/LayoutGridItem';
import { Text } from '../../../../../common/Text';
import { CatalogPurchasedEvent, CatalogPurchaseFailureEvent } from '../../../../../events';
import { CatalogEvent } from '../../../../../events/catalog/CatalogEvent';
import { useUiEvent } from '../../../../../hooks';
import { SendMessageHook } from '../../../../../hooks/messages/message-event';
@ -22,24 +23,24 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
{
const [ pendingOffer, setPendingOffer ] = useState<ClubOfferData>(null);
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
const { currentPage = null, catalogState = null } = useCatalogContext();
const { clubOffers = null, subscriptionInfo = null } = catalogState;
const { currentPage = null, catalogOptions = null } = useCatalogContext();
const { clubOffers = null, subscriptionInfo = null } = catalogOptions;
const onCatalogEvent = useCallback((event: CatalogEvent) =>
{
switch(event.type)
{
case CatalogEvent.PURCHASE_SUCCESS:
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
setPurchaseState(CatalogPurchaseState.NONE);
return;
case CatalogEvent.PURCHASE_FAILED:
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
setPurchaseState(CatalogPurchaseState.FAILED);
return;
}
}, []);
useUiEvent(CatalogEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogEvent.PURCHASE_FAILED, onCatalogEvent);
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
const getOfferText = useCallback((offer: ClubOfferData) =>
{
@ -145,7 +146,7 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
return (
<Grid>
<Column fullHeight size={ 7 } overflow="hidden">
<Column fullHeight size={ 7 } overflow="hidden" justifyContent="between">
<Grid grow columnCount={ 1 } className="nitro-catalog-layout-vip-buy-grid" overflow="auto">
{ clubOffers && (clubOffers.length > 0) && clubOffers.map((offer, index) =>
{
@ -169,9 +170,9 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
</Column>
</LayoutGridItem>
);
})}
<Text dangerouslySetInnerHTML={{ __html: LocalizeText('catalog.vip.buy.hccenter') }}></Text>
}) }
</Grid>
<Text center dangerouslySetInnerHTML={{ __html: LocalizeText('catalog.vip.buy.hccenter') }}></Text>
</Column>
<Column size={ 5 } overflow="hidden">
<Column fullHeight center overflow="hidden">

View File

@ -1,4 +1,3 @@
import { RoomPreviewer } from '@nitrots/nitro-renderer';
import { ICatalogPage } from '../../../common/ICatalogPage';
import { CatalogLayoutProps } from './CatalogLayout.types';
import { CatalogLayoutBadgeDisplayView } from './CatalogLayoutBadgeDisplayView';
@ -20,9 +19,11 @@ import { CatalogLayoutMarketplacePublicItemsView } from './marketplace/CatalogLa
import { CatalogLayoutPetView } from './pets/CatalogLayoutPetView';
import { CatalogLayoutVipGiftsView } from './vip-gifts/CatalogLayoutVipGiftsView';
export const GetCatalogLayout = (page: ICatalogPage, roomPreviewer: RoomPreviewer) =>
export const GetCatalogLayout = (page: ICatalogPage) =>
{
const layoutProps: CatalogLayoutProps = { page, roomPreviewer };
if(!page) return null;
const layoutProps: CatalogLayoutProps = { page };
switch(page.layoutCode)
{
@ -44,8 +45,6 @@ export const GetCatalogLayout = (page: ICatalogPage, roomPreviewer: RoomPreviewe
return <CatalogLayouGuildForumView { ...layoutProps } />;
case 'guild_custom_furni':
return <CatalogLayouGuildCustomFurniView { ...layoutProps } />;
case 'search_results':
return null;
case 'club_gifts':
return <CatalogLayoutVipGiftsView { ...layoutProps } />;
case 'marketplace_own_items':
@ -59,11 +58,11 @@ export const GetCatalogLayout = (page: ICatalogPage, roomPreviewer: RoomPreviewe
case 'spaces_new':
return <CatalogLayoutSpacesView { ...layoutProps } />;
case 'trophies':
return <CatalogLayoutTrophiesView roomPreviewer={roomPreviewer} page={ page } />;
return <CatalogLayoutTrophiesView page={ page } />;
case 'info_loyalty':
return <CatalogLayoutInfoLoyaltyView { ...layoutProps } />;
case 'badge_display':
return <CatalogLayoutBadgeDisplayView roomPreviewer={roomPreviewer} page={ page } />;
return <CatalogLayoutBadgeDisplayView page={ page } />;
//case 'default_3x3_color_grouping':
//return <CatalogLayoutColorGroupingView { ...layoutProps } />;
case 'bots':

View File

@ -18,7 +18,8 @@ export const MarketplacePostOfferView : FC<{}> = props =>
{
const [ item, setItem ] = useState<FurnitureItem>(null);
const [ askingPrice, setAskingPrice ] = useState(0);
const { catalogState = null, dispatchCatalogState = null } = useCatalogContext();
const { catalogOptions = null } = useCatalogContext();
const { marketplaceConfiguration = null } = catalogOptions;
const close = useCallback(() =>
{
@ -105,24 +106,24 @@ export const MarketplacePostOfferView : FC<{}> = props =>
</Column>
<Column overflow="auto">
<Text italics>
{ LocalizeText('inventory.marketplace.make_offer.expiration_info', ['time'], [catalogState.marketplaceConfiguration.offerTime.toString()]) }
{ LocalizeText('inventory.marketplace.make_offer.expiration_info', ['time'], [marketplaceConfiguration.offerTime.toString()]) }
</Text>
<div className="input-group has-validation">
<input className="form-control form-control-sm" type="number" min={ 0 } value={ askingPrice } onChange={ event => setAskingPrice(event.target.valueAsNumber) } placeholder={ LocalizeText('inventory.marketplace.make_offer.price_request') } />
{ ((askingPrice < catalogState.marketplaceConfiguration.minimumPrice) || isNaN(askingPrice)) &&
{ ((askingPrice < marketplaceConfiguration.minimumPrice) || isNaN(askingPrice)) &&
<Base className="invalid-feedback d-block">
{ LocalizeText('inventory.marketplace.make_offer.min_price', [ 'minprice' ], [ catalogState.marketplaceConfiguration.minimumPrice.toString() ]) }
{ LocalizeText('inventory.marketplace.make_offer.min_price', [ 'minprice' ], [ marketplaceConfiguration.minimumPrice.toString() ]) }
</Base> }
{ ((askingPrice > catalogState.marketplaceConfiguration.maximumPrice) && !isNaN(askingPrice)) &&
{ ((askingPrice > marketplaceConfiguration.maximumPrice) && !isNaN(askingPrice)) &&
<Base className="invalid-feedback d-block">
{ LocalizeText('inventory.marketplace.make_offer.max_price', [ 'maxprice' ], [ catalogState.marketplaceConfiguration.maximumPrice.toString() ]) }
{ LocalizeText('inventory.marketplace.make_offer.max_price', [ 'maxprice' ], [ marketplaceConfiguration.maximumPrice.toString() ]) }
</Base> }
{ (!((askingPrice < catalogState.marketplaceConfiguration.minimumPrice) || (askingPrice > catalogState.marketplaceConfiguration.maximumPrice) || isNaN(askingPrice))) &&
{ (!((askingPrice < marketplaceConfiguration.minimumPrice) || (askingPrice > marketplaceConfiguration.maximumPrice) || isNaN(askingPrice))) &&
<Base className="invalid-feedback d-block">
{ LocalizeText('inventory.marketplace.make_offer.final_price', [ 'commission', 'finalprice' ], [ catalogState.marketplaceConfiguration.commission.toString(), (askingPrice + catalogState.marketplaceConfiguration.commission).toString() ]) }
{ LocalizeText('inventory.marketplace.make_offer.final_price', [ 'commission', 'finalprice' ], [ marketplaceConfiguration.commission.toString(), (askingPrice + marketplaceConfiguration.commission).toString() ]) }
</Base> }
</div>
<Button size="sm" disabled={ ((askingPrice < catalogState.marketplaceConfiguration.minimumPrice) || (askingPrice > catalogState.marketplaceConfiguration.maximumPrice) || isNaN(askingPrice)) } onClick={ postItem }>
<Button size="sm" disabled={ ((askingPrice < marketplaceConfiguration.minimumPrice) || (askingPrice > marketplaceConfiguration.maximumPrice) || isNaN(askingPrice)) } onClick={ postItem }>
{ LocalizeText('inventory.marketplace.make_offer.post') }
</Button>
</Column>

View File

@ -4,14 +4,13 @@ import { LocalizeText } from '../../../../../../api';
import { Column } from '../../../../../../common/Column';
import { Flex } from '../../../../../../common/Flex';
import { Text } from '../../../../../../common/Text';
import { CatalogEvent } from '../../../../../../events';
import { CatalogPurchasedEvent } from '../../../../../../events';
import { useUiEvent } from '../../../../../../hooks/events/ui/ui-event';
import { SendMessageHook } from '../../../../../../hooks/messages/message-event';
import { CurrencyIcon } from '../../../../../../views/shared/currency-icon/CurrencyIcon';
import { IPurchasableOffer } from '../../../../common/IPurchasableOffer';
import { Offer } from '../../../../common/Offer';
import { CatalogPurchaseButtonView } from '../../purchase/CatalogPurchaseButtonView';
import { CatalogPurchaseGiftButtonView } from '../../purchase/CatalogPurchaseGiftButtonView';
import { CatalogPurchaseWidgetView } from '../../widgets/CatalogPurchaseWidgetView';
import { CatalogPetNameApprovalView } from './CatalogPetNameApprovalView';
export interface CatalogLayoutPetPurchaseViewProps
@ -27,17 +26,12 @@ export const CatalogLayoutPetPurchaseView: FC<CatalogLayoutPetPurchaseViewProps>
const [ petNameValue, setPetNameValue ] = useState('');
const [ nameApproved, setNameApproved ] = useState(false);
const onCatalogEvent = useCallback((event: CatalogEvent) =>
const onCatalogPurchasedEvent = useCallback((event: CatalogPurchasedEvent) =>
{
switch(event.type)
{
case CatalogEvent.PURCHASE_SUCCESS:
setNameApproved(false);
return;
}
setNameApproved(false);
}, []);
useUiEvent(CatalogEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogPurchasedEvent);
const beforePurchase = useCallback(() =>
{
@ -69,9 +63,10 @@ export const CatalogLayoutPetPurchaseView: FC<CatalogLayoutPetPurchaseViewProps>
</Column>
</Flex>
<Column gap={ 1 }>
<CatalogPurchaseButtonView offer={ offer } pageId={ pageId } extra={ extraData } quantity={ 1 } isPurchaseAllowed={ nameApproved } beforePurchase={ beforePurchase } />
<CatalogPurchaseWidgetView />
{/* <CatalogPurchaseButtonView offer={ offer } pageId={ pageId } extra={ extraData } quantity={ 1 } isPurchaseAllowed={ nameApproved } beforePurchase={ beforePurchase } />
{ offer.giftable &&
<CatalogPurchaseGiftButtonView offer={ offer } pageId={ pageId } extra={ extraData } disabled={ nameApproved } /> }
<CatalogPurchaseGiftButtonView offer={ offer } pageId={ pageId } extra={ extraData } disabled={ nameApproved } /> } */}
</Column>
</Column>
);

View File

@ -1,34 +1,71 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ColorConverter, GetSellablePetPalettesComposer, SellablePetPaletteData } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../../../api';
import { Base } from '../../../../../../common/Base';
import { Button } from '../../../../../../common/Button';
import { Column } from '../../../../../../common/Column';
import { Flex } from '../../../../../../common/Flex';
import { Grid } from '../../../../../../common/Grid';
import { LayoutGridItem } from '../../../../../../common/layout/LayoutGridItem';
import { Text } from '../../../../../../common/Text';
import { BatchUpdates } from '../../../../../../hooks';
import { CatalogNameResultEvent } from '../../../../../../events';
import { CatalogWidgetEvent } from '../../../../../../events/catalog/CatalogWidgetEvent';
import { BatchUpdates, useUiEvent } from '../../../../../../hooks';
import { SendMessageHook } from '../../../../../../hooks/messages/message-event';
import { PetImageView } from '../../../../../../views/shared/pet-image/PetImageView';
import { GetPetAvailableColors, GetPetIndexFromLocalization } from '../../../../common/CatalogUtilities';
import { useCatalogContext } from '../../../../context/CatalogContext';
import { CatalogRoomPreviewerView } from '../../../catalog-room-previewer/CatalogRoomPreviewerView';
import { CatalogPageDetailsView } from '../../../page-details/CatalogPageDetailsView';
import { CatalogAddOnBadgeWidgetView } from '../../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogPurchaseWidgetView } from '../../widgets/CatalogPurchaseWidgetView';
import { CatalogTotalPriceWidget } from '../../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from '../CatalogLayout.types';
import { CatalogLayoutPetPurchaseView } from './CatalogLayoutPetPurchaseView';
export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
{
const { page = null, roomPreviewer = null } = props;
const { page = null } = props;
const [ petIndex, setPetIndex ] = useState(-1);
const [ sellablePalettes, setSellablePalettes ] = useState<SellablePetPaletteData[]>([]);
const [ selectedPaletteIndex, setSelectedPaletteIndex ] = useState(-1);
const [ sellableColors, setSellableColors ] = useState<number[][]>([]);
const [ selectedColorIndex, setSelectedColorIndex ] = useState(-1);
const [ colorsShowing, setColorsShowing ] = useState(false);
const { currentOffer = null, setCurrentOffer = null, catalogState = null, dispatchCatalogState = null } = useCatalogContext();
const { petPalettes = [] } = catalogState;
const [ petName, setPetName ] = useState('');
const [ approvalPending, setApprovalPending ] = useState(true);
const [ approvalResult, setApprovalResult ] = useState(-1);
const { currentOffer = null, setCurrentOffer = null, catalogOptions = null, roomPreviewer = null } = useCatalogContext();
const { petPalettes = [] } = catalogOptions;
const validationErrorMessage = () =>
{
let key: string = '';
switch(approvalResult)
{
case 1:
key = 'catalog.alert.petname.long';
break;
case 2:
key = 'catalog.alert.petname.short';
break;
case 3:
key = 'catalog.alert.petname.chars';
break;
case 4:
key = 'catalog.alert.petname.bobba';
break;
}
return LocalizeText(key);
}
const onCatalogNameResultEvent = useCallback((event: CatalogNameResultEvent) =>
{
setApprovalPending(false);
}, []);
useUiEvent(CatalogWidgetEvent.APPROVE_RESULT, onCatalogNameResultEvent)
const getColor = useMemo(() =>
{
@ -136,7 +173,7 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
{
if(!roomPreviewer) return;
roomPreviewer && roomPreviewer.reset(false);
roomPreviewer.reset(false);
if((petIndex === -1) || !sellablePalettes.length || (selectedPaletteIndex === -1)) return;
@ -164,23 +201,33 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
{ colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => <LayoutGridItem key={ index } itemActive={ (selectedColorIndex === index) } itemColor={ ColorConverter.int2rgb(colorSet[0]) } onClick={ event => setSelectedColorIndex(index) } />) }
</Grid>
</Column>
<Column size={ 5 } overflow="hidden">
{ (petIndex === -1) &&
<CatalogPageDetailsView page={ page } /> }
{ (petIndex >= 0) &&
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<>
<Column overflow="hidden" position="relative" gap={ 0 }>
{ roomPreviewer && <CatalogRoomPreviewerView roomPreviewer={ roomPreviewer } height={ 140 } /> }
{ (petIndex > -1 && petIndex <= 7) &&
<Base position="absolute" className="start-1 bottom-1">
<Button size="sm" active={ colorsShowing } onClick={ event => setColorsShowing(!colorsShowing) }>
<FontAwesomeIcon icon="fill-drip" />
</Button>
</Base> }
</Column>
<Column grow>
<Text grow truncate>{ petBreedName }</Text>
<CatalogLayoutPetPurchaseView offer={ currentOffer } pageId={ page.pageId } extra={ petPurchaseString } />
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Base position="relative" overflow="hidden">
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView position="absolute" className="bg-muted rounded bottom-1 end-1" />
{ ((petIndex > -1) && (petIndex <= 7)) &&
<Button position="absolute" className="bottom-1 start-1" onClick={ event => setColorsShowing(!colorsShowing) }>
<FontAwesomeIcon icon="fill-drip" />
</Button> }
</Base>
<Column grow gap={ 1 }>
<Text truncate>{ petBreedName }</Text>
<Column grow gap={ 1 }>
<input type="text" className="form-control form-control-sm w-100" placeholder={ LocalizeText('widgets.petpackage.name.title') } value={ petName } onChange={ event => setPetName(event.target.value) } />
{ (approvalResult > 0) &&
<Base className="invalid-feedback">{ validationErrorMessage }</Base> }
</Column>
<Flex justifyContent="end">
<CatalogTotalPriceWidget justifyContent="end" alignItems="end" />
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>

View File

@ -6,62 +6,48 @@ import { Text } from '../../../../../../common/Text';
import { SendMessageHook } from '../../../../../../hooks';
import { NotificationUtilities } from '../../../../../../views/notification-center/common/NotificationUtilities';
import { useCatalogContext } from '../../../../context/CatalogContext';
import { CatalogActions } from '../../../../reducers/CatalogReducer';
import { CatalogLayoutProps } from '../CatalogLayout.types';
import { VipGiftItem } from './VipGiftItemView';
export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
{
const { catalogState, dispatchCatalogState } = useCatalogContext();
const { catalogOptions = null, setCatalogOptions = null } = useCatalogContext();
const { clubGifts = null, subscriptionInfo = null } = catalogOptions;
const giftsAvailable = useCallback(() =>
{
const clubGifts = catalogState.clubGifts;
if(!clubGifts) return '';
if(clubGifts.giftsAvailable > 0)
{
return LocalizeText('catalog.club_gift.available', ['amount'], [clubGifts.giftsAvailable.toString()]);
}
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(clubGifts.daysUntilNextGift > 0) return LocalizeText('catalog.club_gift.days_until_next', [ 'days' ], [ clubGifts.daysUntilNextGift.toString() ]);
if(catalogState.subscriptionInfo.isVip)
{
return LocalizeText('catalog.club_gift.not_available');
}
if(subscriptionInfo.isVip) return LocalizeText('catalog.club_gift.not_available');
return LocalizeText('catalog.club_gift.no_club');
}, [catalogState.clubGifts, catalogState.subscriptionInfo.isVip]);
}, [ clubGifts, subscriptionInfo ]);
const selectGift = useCallback((localizationId: string) =>
{
NotificationUtilities.confirm(LocalizeText('catalog.club_gift.confirm'), () =>
{
SendMessageHook(new SelectClubGiftComposer(localizationId));
const prev = catalogState.clubGifts;
prev.giftsAvailable--;
setCatalogOptions(prevValue =>
{
prevValue.clubGifts.giftsAvailable--;
dispatchCatalogState({
type: CatalogActions.SET_CLUB_GIFTS,
payload: {
clubGifts: prev
}
});
return { ...prevValue };
});
}, null);
}, [catalogState.clubGifts, dispatchCatalogState]);
}, [ setCatalogOptions ]);
return (
<>
<Text truncate shrink fontWeight="bold">{ giftsAvailable() }</Text>
<Grid columnCount={ 1 } className="nitro-catalog-layout-vip-gifts-grid" overflow="auto">
{ (catalogState.clubGifts.offers.length > 0) && catalogState.clubGifts.offers.map(offer => <VipGiftItem key={ offer.offerId } offer={ offer } isAvailable={ (catalogState.clubGifts.getOfferExtraData(offer.offerId).isSelectable && (catalogState.clubGifts.giftsAvailable > 0)) } onSelect={ selectGift }/>) }
{ (clubGifts.offers.length > 0) && clubGifts.offers.map(offer => <VipGiftItem key={ offer.offerId } offer={ offer } isAvailable={ (clubGifts.getOfferExtraData(offer.offerId).isSelectable && (clubGifts.giftsAvailable > 0)) } onSelect={ selectGift }/>) }
</Grid>
</>
)

View File

@ -22,5 +22,9 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
return offer.product.getIconUrl(offer);
}, [ offer ]);
return <LayoutGridItem itemImage={ iconUrl } itemCount={ (offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? offer.product.productCount : 1 } { ...rest } />;
const product = offer.product;
if(!product) return null;
return <LayoutGridItem itemImage={ iconUrl } itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) } itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) } itemUniqueNumber={ product.uniqueLimitedItemSeriesSize } { ...rest } />;
}

View File

@ -6,8 +6,6 @@ import { Text } from '../../../../../common/Text';
import { BadgeImageView } from '../../../../../views/shared/badge-image/BadgeImageView';
import { LimitedEditionCompletePlateView } from '../../../../../views/shared/limited-edition/LimitedEditionCompletePlateView';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { Offer } from '../../../common/Offer';
import { CatalogRoomPreviewerView } from '../../catalog-room-previewer/CatalogRoomPreviewerView';
import { CatalogPurchaseView } from '../purchase/CatalogPurchaseView';
export interface CatalogProductPreviewViewProps
@ -28,8 +26,6 @@ export const CatalogProductPreviewView: FC<CatalogProductPreviewViewProps> = pro
return (
<>
<Column overflow="hidden" position="relative" gap={ 0 }>
{ ((offer.pricingModel === Offer.PRICING_MODEL_SINGLE) || (offer.pricingModel === Offer.PRICING_MODEL_MULTI)) &&
<CatalogRoomPreviewerView roomPreviewer={ roomPreviewer } height={ 140 } /> }
{ product.isUniqueLimitedItem &&
<Base fullWidth position="absolute" className="top-1">
<LimitedEditionCompletePlateView className="mx-auto" uniqueLimitedItemsLeft={ product.uniqueLimitedItemsLeft } uniqueLimitedSeriesSize={ product.uniqueLimitedItemSeriesSize } />

View File

@ -1,52 +0,0 @@
import { GetProductOfferComposer, MouseEventType } from '@nitrots/nitro-renderer';
import { FC, MouseEvent, useCallback, useState } from 'react';
import { SendMessageHook } from '../../../../../hooks';
import { FurnitureOffer } from '../../../common/FurnitureOffer';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogProductView } from '../product/CatalogProductView';
export interface CatalogPageOfferViewProps
{
isActive: boolean;
offer: IPurchasableOffer;
}
export const CatalogPageOfferView: FC<CatalogPageOfferViewProps> = props =>
{
const { isActive = false, offer = null } = props;
const [ isMouseDown, setMouseDown ] = useState(false);
const { setCurrentOffer = null } = useCatalogContext();
const onMouseEvent = useCallback((event: MouseEvent) =>
{
switch(event.type)
{
case MouseEventType.MOUSE_CLICK:
if(isActive) return;
setCurrentOffer(offer);
if(offer instanceof FurnitureOffer)
{
SendMessageHook(new GetProductOfferComposer(offer.offerId));
}
return;
case MouseEventType.MOUSE_DOWN:
setMouseDown(true);
return;
case MouseEventType.MOUSE_UP:
setMouseDown(false);
return;
case MouseEventType.ROLL_OUT:
if(!isMouseDown || !isActive) return;
return;
}
}, [ isActive, offer, isMouseDown, setCurrentOffer ]);
const product = offer.product;
if(!product) return null;
return <CatalogProductView itemActive={ isActive } product={ product } onClick={ onMouseEvent } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } />
}

View File

@ -1,23 +0,0 @@
import { FC } from 'react';
import { Grid, GridProps } from '../../../../../common/Grid';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogPageOfferView } from './CatalogPageOfferView';
export interface CatalogPageOffersViewProps extends GridProps
{
offers: IPurchasableOffer[];
}
export const CatalogPageOffersView: FC<CatalogPageOffersViewProps> = props =>
{
const { offers = [], children = null, ...rest } = props;
const { currentOffer = null } = useCatalogContext();
return (
<Grid grow columnCount={ 5 } overflow="auto" { ...rest }>
{ offers && (offers.length > 0) && offers.map((offer, index) => <CatalogPageOfferView key={ index } isActive={ (currentOffer === offer) } offer={ offer } />) }
{ children }
</Grid>
);
}

View File

@ -1,123 +0,0 @@
import { PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { LocalizeText } from '../../../../../api';
import { Button, ButtonProps } from '../../../../../common/Button';
import { CatalogEvent } from '../../../../../events';
import { BatchUpdates } from '../../../../../hooks';
import { useUiEvent } from '../../../../../hooks/events/ui/ui-event';
import { SendMessageHook } from '../../../../../hooks/messages/message-event';
import { LoadingSpinnerView } from '../../../../../layout';
import { GetCurrencyAmount } from '../../../../../views/purse/common/CurrencyHelper';
import { CatalogPurchaseState } from '../../../common/CatalogPurchaseState';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
export interface CatalogPurchaseButtonViewProps extends ButtonProps
{
offer: IPurchasableOffer;
pageId: number;
extra?: string;
quantity?: number;
isPurchaseAllowed?: boolean;
beforePurchase?: () => void;
}
export const CatalogPurchaseButtonView: FC<CatalogPurchaseButtonViewProps> = props =>
{
const { offer = null, pageId = -1, extra = null, quantity = 1, isPurchaseAllowed = true, beforePurchase = null, ...rest } = props;
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
const [ pendingApproval, setPendingApproval ] = useState(false);
const onCatalogEvent = useCallback((event: CatalogEvent) =>
{
switch(event.type)
{
case CatalogEvent.PURCHASE_SUCCESS:
setPurchaseState(CatalogPurchaseState.NONE);
return;
case CatalogEvent.SOLD_OUT:
setPurchaseState(CatalogPurchaseState.SOLD_OUT);
return;
case CatalogEvent.PURCHASE_FAILED:
setPurchaseState(CatalogPurchaseState.FAILED);
return;
}
}, []);
useUiEvent(CatalogEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogEvent.SOLD_OUT, onCatalogEvent);
useUiEvent(CatalogEvent.PURCHASE_FAILED, onCatalogEvent);
const purchase = useCallback(() =>
{
SendMessageHook(new PurchaseFromCatalogComposer(pageId, offer.offerId, extra, quantity));
}, [ pageId, offer, extra, quantity ]);
const attemptPurchase = useCallback(() =>
{
setPurchaseState(CatalogPurchaseState.PURCHASE);
if(beforePurchase) beforePurchase();
if(!isPurchaseAllowed)
{
BatchUpdates(() =>
{
setPendingApproval(true);
setPurchaseState(CatalogPurchaseState.NONE);
});
return;
}
purchase();
}, [ isPurchaseAllowed, beforePurchase, purchase ]);
useEffect(() =>
{
setPurchaseState(CatalogPurchaseState.NONE);
}, [ offer, quantity ]);
useEffect(() =>
{
if(pendingApproval && isPurchaseAllowed)
{
setPendingApproval(false);
purchase();
return;
}
}, [ purchaseState, pendingApproval, isPurchaseAllowed, purchase ]);
const product = offer.product;
if(product && product.isUniqueLimitedItem && !product.uniqueLimitedItemsLeft)
{
return <Button variant="danger" size="sm" disabled>{ LocalizeText('catalog.alert.limited_edition_sold_out.title') }</Button>;
}
if((offer.priceInCredits * quantity) > GetCurrencyAmount(-1))
{
return <Button variant="danger" size="sm" disabled>{ LocalizeText('catalog.alert.notenough.title') }</Button>;
}
if((offer.priceInActivityPoints * quantity) > GetCurrencyAmount(offer.activityPointType))
{
return <Button variant="danger" size="sm" disabled>{ LocalizeText('catalog.alert.notenough.activitypoints.title.' + offer.activityPointType) }</Button>;
}
switch(purchaseState)
{
case CatalogPurchaseState.CONFIRM:
return <Button variant="warning" size="sm" onClick={ attemptPurchase } { ...rest }>{ LocalizeText('catalog.marketplace.confirm_title') }</Button>;
case CatalogPurchaseState.PURCHASE:
return <Button variant="primary" size="sm" disabled { ...rest }><LoadingSpinnerView /></Button>;
case CatalogPurchaseState.SOLD_OUT:
return <Button variant="danger" size="sm" disabled { ...rest }>{ LocalizeText('generic.failed') + ' - ' + LocalizeText('catalog.alert.limited_edition_sold_out.title') }</Button>;
case CatalogPurchaseState.FAILED:
return <Button variant="danger" size="sm" disabled { ...rest }>{ LocalizeText('generic.failed') }</Button>;
case CatalogPurchaseState.NONE:
default:
return <Button variant="success" size="sm" onClick={ event => setPurchaseState(CatalogPurchaseState.CONFIRM) } { ...rest }>{ LocalizeText('buy') }</Button>
}
}

View File

@ -1,25 +0,0 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { Button, ButtonProps } from '../../../../../common/Button';
import { CatalogInitGiftEvent } from '../../../../../events/catalog/CatalogInitGiftEvent';
import { dispatchUiEvent } from '../../../../../hooks';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
export interface CatalogPurchaseGiftButtonViewProps extends ButtonProps
{
offer: IPurchasableOffer;
pageId: number;
extra?: string;
}
export const CatalogPurchaseGiftButtonView: FC<CatalogPurchaseGiftButtonViewProps> = props =>
{
const { offer = null, pageId = -1, extra = null, ...rest } = props;
const initGift = () =>
{
dispatchUiEvent(new CatalogInitGiftEvent(pageId, offer.offerId, extra));
}
return <Button variant="secondary" size="sm" onClick={ initGift } { ...rest }>{ LocalizeText('catalog.purchase_confirmation.gift') }</Button>;
}

View File

@ -7,8 +7,6 @@ import { Text } from '../../../../../common/Text';
import { CurrencyIcon } from '../../../../../views/shared/currency-icon/CurrencyIcon';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { Offer } from '../../../common/Offer';
import { CatalogPurchaseButtonView } from './CatalogPurchaseButtonView';
import { CatalogPurchaseGiftButtonView } from './CatalogPurchaseGiftButtonView';
export interface CatalogPurchaseViewProps
{
@ -82,11 +80,6 @@ export const CatalogPurchaseView: FC<CatalogPurchaseViewProps> = props =>
</Flex> }
</Column>
</Flex>
<Column gap={ 1 }>
<CatalogPurchaseButtonView offer={ offer } pageId={ pageId } extra={ extraData } quantity={ quantity } disabled={ disabled } />
{ offer.giftable &&
<CatalogPurchaseGiftButtonView offer={ offer } pageId={ pageId } extra={ extraData } disabled={ disabled } /> }
</Column>
</Column>
);
}

View File

@ -1,9 +1,7 @@
import { FC, useCallback, useState } from 'react';
import { FC } from 'react';
import { BaseProps } from '../../../../../common/Base';
import { CatalogSelectProductEvent } from '../../../../../events';
import { CatalogWidgetEvent } from '../../../../../events/catalog/CatalogWidgetEvent';
import { useUiEvent } from '../../../../../hooks';
import { BadgeImageView } from '../../../../../views/shared/badge-image/BadgeImageView';
import { useCatalogContext } from '../../../context/CatalogContext';
interface CatalogAddOnBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
{
@ -13,16 +11,9 @@ interface CatalogAddOnBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
export const CatalogAddOnBadgeWidgetView: FC<CatalogAddOnBadgeWidgetViewProps> = props =>
{
const { ...rest } = props;
const [ badgeCode, setBadgeCode ] = useState<string>(null);
const { currentOffer = null } = useCatalogContext();
const onCatalogSelectProductEvent = useCallback((event: CatalogSelectProductEvent) =>
{
if(event.offer.badgeCode) setBadgeCode(event.offer.badgeCode);
}, []);
if(!currentOffer || !currentOffer.badgeCode || !currentOffer.badgeCode.length) return null;
useUiEvent(CatalogWidgetEvent.SELECT_PRODUCT, onCatalogSelectProductEvent);
if(!badgeCode || !badgeCode.length) return null;
return <BadgeImageView badgeCode={ badgeCode } { ...rest } />;
return <BadgeImageView badgeCode={ currentOffer.badgeCode } { ...rest } />;
}

View File

@ -0,0 +1,73 @@
import { StringDataType } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Grid, GridProps } from '../../../../../common/Grid';
import { LayoutGridItem } from '../../../../../common/layout/LayoutGridItem';
import { InventoryBadgesUpdatedEvent } from '../../../../../events';
import { InventoryBadgesRequestEvent } from '../../../../../events/inventory/InventoryBadgesRequestEvent';
import { dispatchUiEvent, useUiEvent } from '../../../../../hooks';
import { BadgeImageView } from '../../../../../views/shared/badge-image/BadgeImageView';
import { useCatalogContext } from '../../../context/CatalogContext';
const EXCLUDED_BADGE_CODES: string[] = [];
interface CatalogBadgeSelectorWidgetViewProps extends GridProps
{
}
export const CatalogBadgeSelectorWidgetView: FC<CatalogBadgeSelectorWidgetViewProps> = props =>
{
const { grow = true, columnCount = 5, overflow = 'auto', ...rest } = props;
const [ badges, setBadges ] = useState<string[]>([]);
const [ currentBadge, setCurrentBadge ] = useState<string>(null);
const { currentOffer = null, setPurchaseOptions = null } = useCatalogContext();
const onInventoryBadgesUpdatedEvent = useCallback((event: InventoryBadgesUpdatedEvent) =>
{
setBadges(event.badges);
}, []);
useUiEvent(InventoryBadgesUpdatedEvent.BADGES_UPDATED, onInventoryBadgesUpdatedEvent);
const previewStuffData = useMemo(() =>
{
if(!currentBadge) return null;
const stuffData = new StringDataType();
stuffData.setValue([ '0', currentBadge, '', '' ]);
return stuffData;
}, [ currentBadge ]);
useEffect(() =>
{
if(!currentOffer) return;
setPurchaseOptions(prevValue =>
{
const extraParamRequired = true;
const extraData = ((previewStuffData && previewStuffData.getValue(1)) || null);
return { ...prevValue, extraParamRequired, extraData, previewStuffData };
});
}, [ currentOffer, previewStuffData, setPurchaseOptions ]);
useEffect(() =>
{
dispatchUiEvent(new InventoryBadgesRequestEvent(InventoryBadgesRequestEvent.REQUEST_BADGES));
}, []);
return (
<Grid grow={ grow } columnCount={ columnCount } overflow={ overflow } { ...rest }>
{ badges && (badges.length > 0) && badges.map(code =>
{
return (
<LayoutGridItem key={ code } itemActive={ (currentBadge === code) } onClick={ event => setCurrentBadge(code) }>
<BadgeImageView badgeCode={ code } />
</LayoutGridItem>
);
}) }
</Grid>
);
}

View File

@ -1,10 +1,6 @@
import { FC, useCallback, useEffect, useState } from 'react';
import { FC } from 'react';
import { Grid, GridProps } from '../../../../../common/Grid';
import { LayoutGridItem } from '../../../../../common/layout/LayoutGridItem';
import { CatalogPageReadyEvent, CatalogSelectProductEvent } from '../../../../../events';
import { CatalogWidgetEvent } from '../../../../../events/catalog/CatalogWidgetEvent';
import { dispatchUiEvent, useUiEvent } from '../../../../../hooks';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { useCatalogContext } from '../../../context/CatalogContext';
interface CatalogBundleGridWidgetViewProps extends GridProps
@ -15,37 +11,13 @@ interface CatalogBundleGridWidgetViewProps extends GridProps
export const CatalogBundleGridWidgetView: FC<CatalogBundleGridWidgetViewProps> = props =>
{
const { children = null, ...rest } = props;
const [ offer, setOffer ] = useState<IPurchasableOffer>(null);
const { currentPage = null } = useCatalogContext();
const { currentOffer = null } = useCatalogContext();
const onCatalogSelectProductEvent = useCallback((event: CatalogSelectProductEvent) =>
{
setOffer(event.offer);
}, []);
useUiEvent(CatalogWidgetEvent.SELECT_PRODUCT, onCatalogSelectProductEvent);
const onCatalogPageReadyEvent = useCallback((event: CatalogPageReadyEvent) =>
{
if(!currentPage || (currentPage.offers.length !== 1)) return;
const offer = currentPage.offers[0];
dispatchUiEvent(new CatalogSelectProductEvent(offer));
}, [ currentPage ]);
useUiEvent(CatalogPageReadyEvent.PAGE_READY, onCatalogPageReadyEvent);
useEffect(() =>
{
return () => setOffer(null);
}, [ currentPage ]);
if(!offer) return null;
if(!currentOffer) return null;
return (
<Grid grow columnCount={ 5 } overflow="auto" { ...rest }>
{ offer.products && (offer.products.length > 0) && offer.products.map((product, index) => <LayoutGridItem key={ index } itemImage={ product.getIconUrl() } itemCount={ product.productCount } />) }
{ currentOffer.products && (currentOffer.products.length > 0) && currentOffer.products.map((product, index) => <LayoutGridItem key={ index } itemImage={ product.getIconUrl() } itemCount={ product.productCount } />) }
{ children }
</Grid>
);

View File

@ -1,20 +1,16 @@
import { FC, useCallback } from 'react';
import { CatalogPageReadyEvent, CatalogSelectProductEvent } from '../../../../../events';
import { dispatchUiEvent, useUiEvent } from '../../../../../hooks';
import { FC, useEffect } from 'react';
import { useCatalogContext } from '../../../context/CatalogContext';
export const CatalogFirstProductSelectorWidgetView: FC<{}> = props =>
{
const { currentPage = null } = useCatalogContext();
const { currentPage = null, setCurrentOffer = null } = useCatalogContext();
const onCatalogPageReadyEvent = useCallback((event: CatalogPageReadyEvent) =>
useEffect(() =>
{
if(!currentPage || !currentPage.offers.length) return;
dispatchUiEvent(new CatalogSelectProductEvent(currentPage.offers[0]));
}, [ currentPage ]);
useUiEvent(CatalogPageReadyEvent.PAGE_READY, onCatalogPageReadyEvent);
setCurrentOffer(currentPage.offers[0]);
}, [ currentPage, setCurrentOffer ]);
return null;
}

View File

@ -0,0 +1,32 @@
import { StringDataType } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { BaseProps } from '../../../../../common/Base';
import { BadgeImageView } from '../../../../../views/shared/badge-image/BadgeImageView';
import { useCatalogContext } from '../../../context/CatalogContext';
interface CatalogGuildBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
{
}
export const CatalogGuildBadgeWidgetView: FC<CatalogGuildBadgeWidgetViewProps> = props =>
{
const { ...rest } = props;
const { currentOffer = null, purchaseOptions = null } = useCatalogContext();
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 <BadgeImageView badgeCode={ badgeCode } isGroup={ true } { ...rest } />;
}

View File

@ -0,0 +1,75 @@
import { CatalogGroupsComposer, StringDataType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../../api';
import { Base } from '../../../../../common/Base';
import { Button } from '../../../../../common/Button';
import { Flex } from '../../../../../common/Flex';
import { SendMessageHook } from '../../../../../hooks';
import { useCatalogContext } from '../../../context/CatalogContext';
export const CatalogGuildSelectorWidgetView: FC<{}> = props =>
{
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
const { currentOffer = null, catalogOptions = null, setPurchaseOptions = null } = useCatalogContext();
const { groups = null } = catalogOptions;
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 extraParamRequired = true;
const extraData = ((previewStuffData && previewStuffData.getValue(1)) || null);
return { ...prevValue, extraParamRequired, extraData, previewStuffData };
});
}, [ currentOffer, previewStuffData, setPurchaseOptions ]);
useEffect(() =>
{
SendMessageHook(new CatalogGroupsComposer());
}, []);
if(!groups || !groups.length)
{
return (
<Base className="bg-muted rounded p-1 text-black text-center">
{ LocalizeText('catalog.guild_selector.members_only') }
<Button className="mt-1">
{ LocalizeText('catalog.guild_selector.find_groups') }
</Button>
</Base>
);
}
const selectedGroup = groups[selectedGroupIndex];
return (
<Flex gap={ 1 }>
{ !!selectedGroup &&
<Flex overflow="hidden" className="rounded border">
<Base fullHeight style={ { width: '20px', backgroundColor: '#' + selectedGroup.colorA } } />
<Base fullHeight style={ { width: '20px', backgroundColor: '#' + selectedGroup.colorB } } />
</Flex> }
<select className="form-select form-select-sm" value={ selectedGroupIndex } onChange={ event => setSelectedGroupIndex(parseInt(event.target.value)) }>
{ groups.map((group, index) => <option key={ index } value={ index }>{ group.groupName }</option>) }
</select>
</Flex>
);
}

View File

@ -1,6 +1,6 @@
import { FC } from 'react';
import { Grid, GridProps } from '../../../../../common/Grid';
import { CatalogSelectProductEvent, CatalogSetExtraPurchaseParameterEvent } from '../../../../../events';
import { CatalogSetExtraPurchaseParameterEvent } from '../../../../../events';
import { dispatchUiEvent } from '../../../../../hooks';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { ProductTypeEnum } from '../../../common/ProductTypeEnum';
@ -23,8 +23,6 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
{
setCurrentOffer(offer);
dispatchUiEvent(new CatalogSelectProductEvent(offer));
if(offer.product && (offer.product.productType === ProductTypeEnum.WALL))
{
dispatchUiEvent(new CatalogSetExtraPurchaseParameterEvent(offer.product.extraParam));
@ -35,7 +33,7 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
<Grid grow columnCount={ 5 } overflow="auto" { ...rest }>
{ currentPage.offers && (currentPage.offers.length > 0) && currentPage.offers.map((offer, index) =>
{
return <CatalogGridOfferView key={ index } itemActive={ (currentOffer === offer) } offer={ offer } onClick={ event => selectOffer(offer) } />;
return <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.offerId === offer.offerId)) } offer={ offer } onClick={ event => selectOffer(offer) } />;
}) }
{ children }
</Grid>

View File

@ -0,0 +1,20 @@
import { FC } from 'react';
import { Base, BaseProps } from '../../../../../common/Base';
import { LimitedEditionCompletePlateView } from '../../../../../views/shared/limited-edition/LimitedEditionCompletePlateView';
import { Offer } from '../../../common/Offer';
import { useCatalogContext } from '../../../context/CatalogContext';
export const CatalogLimitedItemWidgetView: FC<BaseProps<HTMLDivElement>> = props =>
{
const { children = null, ...rest } = props;
const { currentOffer = null } = useCatalogContext();
if(!currentOffer || (currentOffer.pricingModel !== Offer.PRICING_MODEL_SINGLE) || !currentOffer.product.isUniqueLimitedItem) return null;
return (
<Base { ...rest }>
<LimitedEditionCompletePlateView className="mx-auto" uniqueLimitedItemsLeft={ currentOffer.product.uniqueLimitedItemsLeft } uniqueLimitedSeriesSize={ currentOffer.product.uniqueLimitedItemSeriesSize } />
{ children }
</Base>
);
}

View File

@ -4,32 +4,36 @@ import { Flex } from '../../../../../common/Flex';
import { Text } from '../../../../../common/Text';
import { CurrencyIcon } from '../../../../../views/shared/currency-icon/CurrencyIcon';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { useCatalogContext } from '../../../context/CatalogContext';
interface CatalogPriceDisplayWidgetViewProps
{
offer: IPurchasableOffer;
separator?: boolean;
}
export const CatalogPriceDisplayWidgetView: FC<CatalogPriceDisplayWidgetViewProps> = props =>
{
const { offer = null } = props;
const { offer = null, separator = false } = props;
const { purchaseOptions = null } = useCatalogContext();
const { quantity = 1 } = purchaseOptions;
if(!offer) return null;
return (
<Flex gap={ 1 } className="bg-muted p-1 rounded">
<>
{ (offer.priceInCredits > 0) &&
<Flex alignItems="center" justifyContent="end" gap={ 1 }>
<Text>{ offer.priceInCredits }</Text>
<Flex alignItems="center" gap={ 1 }>
<Text bold>{ (offer.priceInCredits * quantity) }</Text>
<CurrencyIcon type={ -1 } />
</Flex> }
{ ( offer.priceInCredits > 0) && (offer.priceInActivityPoints > 0) &&
<FontAwesomeIcon icon="plus" /> }
{ separator && (offer.priceInCredits > 0) && (offer.priceInActivityPoints > 0) &&
<FontAwesomeIcon size="xs" color="black" icon="plus" /> }
{ (offer.priceInActivityPoints > 0) &&
<Flex alignItems="center" justifyContent="end" gap={ 1 }>
<Text>{ offer.priceInActivityPoints }</Text>
<Flex alignItems="center" gap={ 1 }>
<Text bold>{ (offer.priceInActivityPoints * quantity) }</Text>
<CurrencyIcon type={ offer.activityPointType } />
</Flex> }
</Flex>
</>
);
}

View File

@ -1,15 +1,16 @@
import { IObjectData } from '@nitrots/nitro-renderer';
import { PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../../api';
import { CreateLinkEvent, GetClubMemberLevel, LocalizeText } from '../../../../../api';
import { Button } from '../../../../../common/Button';
import { CatalogSelectProductEvent, CatalogSetExtraPurchaseParameterEvent } from '../../../../../events';
import { CatalogEvent, CatalogPurchasedEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent } from '../../../../../events';
import { CatalogInitPurchaseEvent } from '../../../../../events/catalog/CatalogInitPurchaseEvent';
import { CatalogPurchaseOverrideEvent } from '../../../../../events/catalog/CatalogPurchaseOverrideEvent';
import { CatalogSetRoomPreviewerStuffDataEvent } from '../../../../../events/catalog/CatalogSetRoomPreviewerStuffDataEvent';
import { CatalogWidgetEvent } from '../../../../../events/catalog/CatalogWidgetEvent';
import { useUiEvent } from '../../../../../hooks';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { SendMessageHook, useUiEvent } from '../../../../../hooks';
import { LoadingSpinnerView } from '../../../../../layout';
import { GetCurrencyAmount } from '../../../../../views/purse/common/CurrencyHelper';
import { CatalogPurchaseState } from '../../../common/CatalogPurchaseState';
import { Offer } from '../../../common/Offer';
import { useCatalogContext } from '../../../context/CatalogContext';
interface CatalogPurchaseWidgetViewProps
{
@ -19,105 +20,142 @@ interface CatalogPurchaseWidgetViewProps
export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = props =>
{
const { noGiftOption = false } = props;
const [ offer, setOffer ] = useState<IPurchasableOffer>(null);
const [ quantity, setQuantity ] = useState(1);
const [ extraData, setExtraData ] = useState<string>('');
const [ extraParamRequired, setExtraParamRequired ] = useState(false);
const [ giftEnabled, setGiftEnabled ] = useState(false);
const [ purchaseCallback, setPurchaseCallback ] = useState<Function>(null);
const [ previewStuffData, setPreviewStuffData ] = useState<IObjectData>(null);
const [ purchaseWillBeGift, setPurchaseWillBeGift ] = useState(false);
const onCatalogSelectProductEvent = useCallback((event: CatalogSelectProductEvent) =>
{
setOffer(event.offer);
}, []);
useUiEvent(CatalogWidgetEvent.SELECT_PRODUCT, onCatalogSelectProductEvent);
const onCatalogSetExtraPurchaseParameterEvent = useCallback((event: CatalogSetExtraPurchaseParameterEvent) =>
{
setExtraData(event.parameter);
setGiftEnabled(offer && offer.giftable);
}, [ offer ]);
useUiEvent(CatalogWidgetEvent.SET_EXTRA_PARM, onCatalogSetExtraPurchaseParameterEvent);
const onCatalogPurchaseOverrideEvent = useCallback((event: CatalogPurchaseOverrideEvent) =>
{
setPurchaseCallback(event.callback);
}, []);
useUiEvent(CatalogWidgetEvent.PURCHASE_OVERRIDE, onCatalogPurchaseOverrideEvent);
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
const { currentOffer = null, currentPage = null, purchaseOptions = null, setPurchaseOptions = null, getNodesByOfferId = null } = useCatalogContext();
const { quantity = 1, extraData = '', extraParamRequired = false, purchaseCallback = null, previewStuffData = null } = purchaseOptions;
const onCatalogInitPurchaseEvent = useCallback((event: CatalogInitPurchaseEvent) =>
{
if(!offer) return;
if(!currentOffer) return;
// show purchase confirmation
// offer, page.pageId, extraData, quantity, previewStuffData, null, true, null
}, [ offer ]);
}, [ currentOffer ]);
useUiEvent(CatalogWidgetEvent.INIT_PURCHASE, onCatalogInitPurchaseEvent);
const onCatalogSetRoomPreviewerStuffDataEvent = useCallback((event: CatalogSetRoomPreviewerStuffDataEvent) =>
const onCatalogEvent = useCallback((event: CatalogEvent) =>
{
setPreviewStuffData(event.stuffData);
switch(event.type)
{
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
setPurchaseState(CatalogPurchaseState.NONE);
return;
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
setPurchaseState(CatalogPurchaseState.FAILED);
return;
case CatalogPurchaseNotAllowedEvent.NOT_ALLOWED:
setPurchaseState(CatalogPurchaseState.FAILED);
return;
case CatalogPurchaseSoldOutEvent.SOLD_OUT:
setPurchaseState(CatalogPurchaseState.SOLD_OUT);
return;
}
}, []);
useUiEvent(CatalogWidgetEvent.SET_PREVIEWER_STUFFDATA, onCatalogSetRoomPreviewerStuffDataEvent);
const onCatalogWidgetEvent = useCallback((event: CatalogWidgetEvent) =>
{
setExtraParamRequired(true);
}, []);
useUiEvent(CatalogWidgetEvent.EXTRA_PARAM_REQUIRED_FOR_BUY, onCatalogWidgetEvent);
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
useUiEvent(CatalogPurchaseNotAllowedEvent.NOT_ALLOWED, onCatalogEvent);
useUiEvent(CatalogPurchaseSoldOutEvent.SOLD_OUT, onCatalogEvent);
const isLimitedSoldOut = useMemo(() =>
{
if(!offer) return false;
if(!currentOffer) return false;
if(extraParamRequired && (!extraData || !extraData.length)) return false;
if(offer.pricingModel === Offer.PRICING_MODEL_SINGLE)
if(currentOffer.pricingModel === Offer.PRICING_MODEL_SINGLE)
{
const product = offer.product;
const product = currentOffer.product;
if(product && product.isUniqueLimitedItem) return !product.uniqueLimitedItemsLeft;
}
return false;
}, [ offer, extraParamRequired, extraData ]);
}, [ currentOffer, extraParamRequired, extraData ]);
const purchase = useCallback((isGift: boolean = false) =>
{
if(!currentOffer) return;
}, []);
if(GetClubMemberLevel() < currentOffer.clubLevel)
{
CreateLinkEvent('habboUI/open/hccenter');
return;
}
if(purchaseCallback)
{
purchaseCallback();
return;
}
let pageId = currentOffer.page.pageId;
if(pageId === -1)
{
const nodes = getNodesByOfferId(currentOffer.offerId);
if(nodes) pageId = nodes[0].pageId;
}
SendMessageHook(new PurchaseFromCatalogComposer(pageId, currentOffer.offerId, extraData, quantity));
}, [ currentOffer, purchaseCallback, extraData, quantity, getNodesByOfferId ]);
// dispatchUiEvent(new CatalogInitGiftEvent(pageId, offer.offerId, extra)); setup gift
useEffect(() =>
{
setQuantity(1);
setPurchaseWillBeGift(false);
}, [ offer ]);
if(!currentOffer) return;
if(!offer) return null;
return () =>
{
setPurchaseState(CatalogPurchaseState.NONE);
setPurchaseOptions({ quantity: 1, extraData: '', extraParamRequired: false, purchaseCallback: null, previewStuffData: null });
}
}, [ currentOffer, setPurchaseOptions ]);
const getPurchaseButton = () =>
if(!currentOffer) return null;
const PurchaseButton = () =>
{
const priceCredits = (offer.priceInCredits * quantity);
const pricePoints = (offer.priceInActivityPoints * quantity);
const priceCredits = (currentOffer.priceInCredits * quantity);
const pricePoints = (currentOffer.priceInActivityPoints * quantity);
if(GetClubMemberLevel() < currentOffer.clubLevel) return <Button variant="danger" disabled>{ LocalizeText('catalog.alert.hc.required') }</Button>;
if(isLimitedSoldOut) return <Button variant="danger" disabled>{ LocalizeText('catalog.alert.limited_edition_sold_out.title') }</Button>;
if(priceCredits > GetCurrencyAmount(-1)) return <Button variant="danger" disabled>{ LocalizeText('catalog.alert.notenough.title') }</Button>;
if(pricePoints > GetCurrencyAmount(currentOffer.activityPointType)) return <Button variant="danger" disabled>{ LocalizeText('catalog.alert.notenough.activitypoints.title.' + currentOffer.activityPointType) }</Button>;
switch(purchaseState)
{
case CatalogPurchaseState.CONFIRM:
return <Button onClick={ event => purchase() }>{ LocalizeText('catalog.marketplace.confirm_title') }</Button>;
case CatalogPurchaseState.PURCHASE:
return <Button disabled><LoadingSpinnerView /></Button>;
case CatalogPurchaseState.FAILED:
return <Button variant="danger">{ LocalizeText('generic.failed') }</Button>;
case CatalogPurchaseState.SOLD_OUT:
return <Button variant="danger">{ LocalizeText('generic.failed') + ' - ' + LocalizeText('catalog.alert.limited_edition_sold_out.title') }</Button>;
case CatalogPurchaseState.NONE:
default:
return <Button disabled={ (extraParamRequired && (!extraData || !extraData.length)) } onClick={ event => setPurchaseState(CatalogPurchaseState.CONFIRM) }>{ LocalizeText('catalog.purchase_confirmation.' + (currentOffer.isRentOffer ? 'rent' : 'buy')) }</Button>;
}
}
return (
<>
<Button disabled={ (isLimitedSoldOut || (extraParamRequired && (!extraData || !extraData.length))) }>
{ LocalizeText('catalog.purchase_confirmation.' + (offer.isRentOffer ? 'rent' : 'buy')) }
</Button>
{ (!noGiftOption && !offer.isRentOffer) &&
<Button disabled={ ((quantity > 1) || !offer.giftable || isLimitedSoldOut || (extraParamRequired && (!extraData || !extraData.length))) }>
{ LocalizeText('catalog.purchase_confirmation.gift') }
</Button> }
<PurchaseButton />
{ (!noGiftOption && !currentOffer.isRentOffer) &&
<Button disabled={ ((quantity > 1) || !currentOffer.giftable || isLimitedSoldOut || (extraParamRequired && (!extraData || !extraData.length))) }>
{ LocalizeText('catalog.purchase_confirmation.gift') }
</Button> }
</>
);
}

View File

@ -1,27 +1,21 @@
import { FC, useCallback, useState } from 'react';
import { BaseProps } from '../../../../../common/Base';
import { CatalogSelectProductEvent } from '../../../../../events';
import { CatalogWidgetEvent } from '../../../../../events/catalog/CatalogWidgetEvent';
import { useUiEvent } from '../../../../../hooks';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { FC } from 'react';
import { Flex, FlexProps } from '../../../../../common/Flex';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
interface CatalogSimplePriceWidgetViewProps extends BaseProps<HTMLDivElement>
interface CatalogSimplePriceWidgetViewProps extends FlexProps
{
}
export const CatalogSimplePriceWidgetView: FC<CatalogSimplePriceWidgetViewProps> = props =>
{
const { ...rest } = props;
const [ offer, setOffer ] = useState<IPurchasableOffer>(null);
const { gap = 1, ...rest } = props;
const { currentOffer = null } = useCatalogContext();
const onCatalogSelectProductEvent = useCallback((event: CatalogSelectProductEvent) =>
{
setOffer(event.offer);
}, []);
useUiEvent(CatalogWidgetEvent.SELECT_PRODUCT, onCatalogSelectProductEvent);
return <CatalogPriceDisplayWidgetView offer={ offer } { ...rest } />;
return (
<Flex gap={ gap } alignItems="center" classNames={ [ 'bg-muted', 'p-1', 'rounded' ] } { ...rest }>
<CatalogPriceDisplayWidgetView separator={ true } offer={ currentOffer } />
</Flex>
);
}

View File

@ -0,0 +1,117 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText } from '../../../../../api';
import { Button } from '../../../../../common/Button';
import { ButtonGroup } from '../../../../../common/ButtonGroup';
import { Grid, GridProps } from '../../../../../common/Grid';
import { BatchUpdates } from '../../../../../hooks';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { Offer } from '../../../common/Offer';
import { ProductTypeEnum } from '../../../common/ProductTypeEnum';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogGridOfferView } from '../offers/CatalogGridOfferView';
interface CatalogSpacesWidgetViewProps extends GridProps
{
}
const SPACES_GROUP_NAMES = [ 'floors', 'walls', 'views' ];
export const CatalogSpacesWidgetView: FC<CatalogSpacesWidgetViewProps> = props =>
{
const { children = null, ...rest } = props;
const [ groupedOffers, setGroupedOffers ] = useState<IPurchasableOffer[][]>(null);
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState(-1);
const [ selectedOfferForGroup, setSelectedOfferForGroup ] = useState<IPurchasableOffer[]>(null);
const { currentPage = null, currentOffer = null, setCurrentOffer = null, setPurchaseOptions = null } = useCatalogContext();
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;
}
}
BatchUpdates(() =>
{
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 extraData = selectedOfferForGroup[selectedGroupIndex].product.extraParam;
const extraParamRequired = true;
return { ...prevValue, extraData, extraParamRequired };
});
}, [ currentOffer, selectedGroupIndex, selectedOfferForGroup, setPurchaseOptions ]);
if(!groupedOffers || (selectedGroupIndex === -1)) return null;
const offers = groupedOffers[selectedGroupIndex];
return (
<>
<ButtonGroup>
{ SPACES_GROUP_NAMES.map((name, index) => <Button key={ index } active={ (selectedGroupIndex === index) } onClick={ event => setSelectedGroupIndex(index) }>{ LocalizeText(`catalog.spaces.tab.${ name }`) }</Button>) }
</ButtonGroup>
<Grid grow columnCount={ 5 } overflow="auto" { ...rest }>
{ offers && (offers.length > 0) && offers.map((offer, index) =>
{
const setSelectedOffer = () =>
{
setSelectedOfferForGroup(prevValue =>
{
const newValue = [ ...prevValue ];
newValue[selectedGroupIndex] = offer;
return newValue;
});
}
return <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer === offer)) } offer={ offer } onClick={ setSelectedOffer } />;
}) }
{ children }
</Grid>
</>
);
}

View File

@ -0,0 +1,43 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { Flex } from '../../../../../common/Flex';
import { Text } from '../../../../../common/Text';
import { useCatalogContext } from '../../../context/CatalogContext';
const MIN_VALUE: number = 1;
const MAX_VALUE: number = 100;
export const CatalogSpinnerWidgetView: FC<{}> = props =>
{
const { currentOffer = null, purchaseOptions = null, setPurchaseOptions = null } = useCatalogContext();
const { quantity = 1 } = purchaseOptions;
const updateQuantity = (value: number) =>
{
value = Math.max(value, MIN_VALUE);
value = Math.min(value, MAX_VALUE);
if(value === quantity) return;
setPurchaseOptions(prevValue =>
{
const quantity = value;
return { ...prevValue, quantity };
});
}
if(!currentOffer || !currentOffer.bundlePurchaseAllowed) return null;
return (
<>
<Text>{ LocalizeText('catalog.bundlewidget.spinner.select.amount') }</Text>
<Flex alignItems="center" gap={ 1 }>
<FontAwesomeIcon icon="caret-left" className="text-black cursor-pointer" onClick={ event => updateQuantity(quantity - 1) } />
<input type="number" className="form-control form-control-sm quantity-input" value={ quantity } onChange={ event => updateQuantity(event.target.valueAsNumber)} />
<FontAwesomeIcon icon="caret-right" className="text-black cursor-pointer" onClick={ event => updateQuantity(quantity + 1) } />
</Flex>
</>
);
}

View File

@ -0,0 +1,20 @@
import { FC } from 'react';
import { Column, ColumnProps } from '../../../../../common/Column';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
interface CatalogSimplePriceWidgetViewProps extends ColumnProps
{
}
export const CatalogTotalPriceWidget: FC<CatalogSimplePriceWidgetViewProps> = props =>
{
const { gap = 1, ...rest } = props;
const { currentOffer = null } = useCatalogContext();
return (
<Column gap={ gap } { ...rest }>
<CatalogPriceDisplayWidgetView offer={ currentOffer } />
</Column>
);
}

View File

@ -1,38 +1,106 @@
import { IObjectData } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { CatalogSelectProductEvent } from '../../../../../events';
import { CatalogSetRoomPreviewerStuffDataEvent } from '../../../../../events/catalog/CatalogSetRoomPreviewerStuffDataEvent';
import { CatalogWidgetEvent } from '../../../../../events/catalog/CatalogWidgetEvent';
import { BatchUpdates, useUiEvent } from '../../../../../hooks';
import { IPurchasableOffer } from '../../../common/IPurchasableOffer';
import { Vector3d } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react';
import { GetAvatarRenderManager, GetSessionDataManager } from '../../../../../api';
import { Column } from '../../../../../common/Column';
import { Grid } from '../../../../../common/Grid';
import { LayoutGridItem } from '../../../../../common/layout/LayoutGridItem';
import { RoomPreviewerView } from '../../../../../views/shared/room-previewer/RoomPreviewerView';
import { FurniCategory } from '../../../common/FurniCategory';
import { Offer } from '../../../common/Offer';
import { ProductTypeEnum } from '../../../common/ProductTypeEnum';
import { useCatalogContext } from '../../../context/CatalogContext';
export const CatalogViewProductWidgetView: FC<{}> = props =>
{
const [ selectedProductEvent, setSelectedProductEvent ] = useState<CatalogSelectProductEvent>(null);
const [ offer, setOffer ] = useState<IPurchasableOffer>(null);
const [ stuffData, setStuffData ] = useState<IObjectData>(null);
const { roomPreviewer = null } = useCatalogContext();
const { currentOffer = null, roomPreviewer = null, purchaseOptions = null } = useCatalogContext();
const { previewStuffData = null } = purchaseOptions;
const onCatalogSelectProductEvent = useCallback((event: CatalogSelectProductEvent) =>
useEffect(() =>
{
BatchUpdates(() =>
if(!currentOffer || (currentOffer.pricingModel === Offer.PRICING_MODEL_BUNDLE) || !roomPreviewer) return;
const product = currentOffer.product;
if(!product) return;
switch(product.productType)
{
setSelectedProductEvent(event);
setOffer(event.offer);
})
}, []);
case ProductTypeEnum.FLOOR: {
if(!product.furnitureData) return;
useUiEvent(CatalogWidgetEvent.SELECT_PRODUCT, onCatalogSelectProductEvent);
if(product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET)
{
const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id);
const customParts = furniData.customParams.split(',').map(value => parseInt(value));
const figureSets: number[] = [];
const onCatalogSetRoomPreviewerStuffDataEvent = useCallback((event: CatalogSetRoomPreviewerStuffDataEvent) =>
for(const part of customParts)
{
if(GetAvatarRenderManager().isValidFigureSetForGender(part, GetSessionDataManager().gender)) figureSets.push(part);
}
const figureString = GetAvatarRenderManager().getFigureStringWithFigureIds(GetSessionDataManager().figure, GetSessionDataManager().gender, 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;
switch(product.furnitureData.specialType)
{
case FurniCategory.FLOOR:
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(product.extraParam);
return;
case FurniCategory.WALL_PAPER:
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(null, product.extraParam);
return;
case FurniCategory.LANDSCAPE: {
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(null, null, product.extraParam);
const furniData = GetSessionDataManager().getWallItemDataByName('ads_twi_windw');
if(furniData) roomPreviewer.addWallItemIntoRoom(furniData.id, new Vector3d(90), furniData.customParams);
return;
}
default:
roomPreviewer.updateObjectRoom('default', 'default', 'default');
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;
}
}, [ currentOffer, previewStuffData, roomPreviewer ]);
if(!currentOffer) return null;
if(currentOffer.pricingModel === Offer.PRICING_MODEL_BUNDLE)
{
setStuffData(event.stuffData);
return (
<Column fit overflow="hidden" className="bg-muted p-2 rounded">
<Grid fullWidth grow columnCount={ 4 } overflow="auto" className="nitro-catalog-layout-bundle-grid">
{ (currentOffer.products.length > 0) && currentOffer.products.map((product, index) =>
{
return <LayoutGridItem key={ index } itemImage={ product.getIconUrl(currentOffer) } itemCount={ product.productCount } />;
}) }
</Grid>
</Column>
);
}
if(roomPreviewer) roomPreviewer.reset(false);
}, [ roomPreviewer ]);
useUiEvent(CatalogWidgetEvent.SET_PREVIEWER_STUFFDATA, onCatalogSetRoomPreviewerStuffDataEvent);
return null;
return <RoomPreviewerView roomPreviewer={ roomPreviewer } height={ 140 } />;
}

View File

@ -19,10 +19,31 @@ import { useCatalogContext } from '../../context/CatalogContext';
export const CatalogSearchView: FC<{}> = props =>
{
const [ searchValue, setSearchValue ] = useState('');
const [ needsProcessing, setNeedsProcessing ] = useState(false);
const { currentType = null, rootNode = null, setActiveNodes = null, offersToNodes = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalogContext();
const updateSearchValue = (value: string) =>
{
BatchUpdates(() =>
{
if(!value || !value.length)
{
setSearchValue('');
if(searchResult) setSearchResult(null);
}
else
{
setSearchValue(value);
setNeedsProcessing(true);
}
});
}
const processSearch = useCallback((search: string) =>
{
setNeedsProcessing(false);
search = search.toLocaleLowerCase().replace(' ', '');
if(!search || !search.length) return;
@ -62,7 +83,7 @@ export const CatalogSearchView: FC<{}> = props =>
{
if(searchValues.indexOf(search) >= 0) foundFurniture.push(furniture);
if(searchValues.length === 250) break;
if(foundFurniture.length === 250) break;
}
}
}
@ -79,28 +100,31 @@ export const CatalogSearchView: FC<{}> = props =>
{
setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1) as ICatalogPage));
setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible))));
setActiveNodes(prevValue => prevValue.slice(0, 1));
});
}, [ offersToNodes, currentType, rootNode, setCurrentPage, setSearchResult, setActiveNodes ]);
}, [ offersToNodes, currentType, rootNode, setCurrentPage, setSearchResult ]);
useEffect(() =>
{
if(!searchValue) return;
if(!needsProcessing) return;
const timeout = setTimeout(() => processSearch(searchValue), 300);
return () =>
{
clearTimeout(timeout);
}
}, [ searchValue, processSearch ]);
return () => clearTimeout(timeout);
}, [ searchValue, needsProcessing, processSearch ]);
return (
<Flex gap={ 1 }>
<input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('generic.search') } value={ searchValue } onChange={ event => setSearchValue(event.target.value) } onKeyDown={ event => (event.code === 'Enter') && processSearch(searchValue) } />
<Button variant="primary" size="sm">
<FontAwesomeIcon icon="search" />
</Button>
<Flex fullWidth alignItems="center" position="relative">
<input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('generic.search') } value={ searchValue } onChange={ event => updateSearchValue(event.target.value) } onKeyDown={ event => ((event.code === 'Enter') || (event.code === 'NumpadEnter')) && processSearch(searchValue) } />
</Flex>
{ (!searchValue || !searchValue.length) &&
<Button variant="primary" size="sm" className="catalog-search-button">
<FontAwesomeIcon icon="search" />
</Button> }
{ searchValue && !!searchValue.length &&
<Button variant="primary" size="sm" className="catalog-search-button" onClick={ event => updateSearchValue('') }>
<FontAwesomeIcon icon="times" />
</Button> }
</Flex>
);
}

View File

@ -1,44 +0,0 @@
import { Dispatch, FC, SetStateAction } from 'react';
import { LocalizeText } from '../../../../api';
import { Base } from '../../../../common/Base';
import { Button } from '../../../../common/Button';
import { Flex } from '../../../../common/Flex';
import { Text } from '../../../../common/Text';
import { useCatalogContext } from '../../context/CatalogContext';
export interface CatalogSelectGroupViewProps
{
selectedGroupIndex: number;
setSelectedGroupIndex: Dispatch<SetStateAction<number>>;
}
export const CatalogSelectGroupView: FC<CatalogSelectGroupViewProps> = props =>
{
const { selectedGroupIndex = -1, setSelectedGroupIndex = null } = props;
const { catalogState = null } = useCatalogContext();
const { groups = null } = catalogState;
if(!groups || !groups.length)
{
return (
<Text grow center className="bg-muted rounded p-1">
{ LocalizeText('catalog.guild_selector.members_only') }
<Button variant="primary" size="sm" className="mt-1">
{ LocalizeText('catalog.guild_selector.find_groups') }
</Button>
</Text>
);
}
return (
<Flex gap={ 1 }>
<Flex overflow="hidden" className="rounded border">
<Base fullHeight style={ { width: '20px', backgroundColor: '#' + groups[selectedGroupIndex].colorA } } />
<Base fullHeight style={ { width: '20px', backgroundColor: '#' + groups[selectedGroupIndex].colorB } } />
</Flex>
<select className="form-select form-select-sm" value={ selectedGroupIndex } onChange={ event => setSelectedGroupIndex(parseInt(event.target.value)) }>
{ groups.map((group, index) => <option key={ index } value={ index }>{ group.groupName }</option>) }
</select>
</Flex>
);
}

View File

@ -1,30 +0,0 @@
import { Dispatch, FC, SetStateAction } from 'react';
import { NitroCardTabsItemView } from '../../../../layout';
import { ICatalogNode } from '../../common/ICatalogNode';
interface CatalogTabsViewsProps
{
node: ICatalogNode;
currentTab: ICatalogNode;
setCurrentTab: Dispatch<SetStateAction<ICatalogNode>>;
}
export const CatalogTabsViews: FC<CatalogTabsViewsProps> = props =>
{
const { node = null, currentTab = null, setCurrentTab = null } = props;
return (
<>
{ node && (node.children.length > 0) && node.children.map(child =>
{
if(!child.isVisible) return null;
return (
<NitroCardTabsItemView key={ child.pageId } isActive={ (currentTab === child) } onClick={ event => setCurrentTab(child) }>
{ child.localization }
</NitroCardTabsItemView>
);
}) }
</>
);
}

View File

@ -5,8 +5,6 @@ export class CatalogEvent extends NitroEvent
public static SHOW_CATALOG: string = 'CE_SHOW_CATALOG';
public static HIDE_CATALOG: string = 'CE_HIDE_CATALOG';
public static TOGGLE_CATALOG: string = 'CE_TOGGLE_CATALOG';
public static PURCHASE_SUCCESS: string = 'CE_PURCHASE_SUCCESS';
public static PURCHASE_FAILED: string = 'CE_PURCHASE_FAILED';
public static SOLD_OUT: string = 'CE_SOLD_OUT';
public static APPROVE_NAME_RESULT: string = 'CE_APPROVE_NAME_RESULT';
public static GIFT_RECEIVER_NOT_FOUND: string = 'CE_GIFT_RECEIVER_NOT_FOUND';

View File

@ -1,13 +1,14 @@
import { CatalogEvent } from './CatalogEvent';
import { NitroEvent } from '@nitrots/nitro-renderer';
import { CatalogWidgetEvent } from './CatalogWidgetEvent';
export class CatalogNameResultEvent extends CatalogEvent
export class CatalogNameResultEvent extends NitroEvent
{
private _result: number;
private _validationInfo: string;
constructor(result: number, validationInfo: string)
{
super(CatalogEvent.APPROVE_NAME_RESULT);
super(CatalogWidgetEvent.APPROVE_RESULT);
this._result = result;
this._validationInfo = validationInfo;

View File

@ -1,27 +0,0 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
export class CatalogPageOpenedEvent extends NitroEvent
{
public static CATALOG_PAGE_OPENED: string = 'CPOE_CATALOG_PAGE_OPENED';
private _pageId: number;
private _localization: string;
constructor(pageId: number, localization: string)
{
super(CatalogPageOpenedEvent.CATALOG_PAGE_OPENED);
this._pageId = pageId;
this._localization = localization;
}
public get pageId(): number
{
return this._pageId;
}
public get localization(): string
{
return this._localization;
}
}

View File

@ -1,11 +0,0 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
export class CatalogPageReadyEvent extends NitroEvent
{
public static PAGE_READY: string = 'CPRE_PAGE_READY';
constructor()
{
super(CatalogPageReadyEvent.PAGE_READY);
}
}

View File

@ -1,12 +1,14 @@
import { CatalogEvent } from './CatalogEvent';
import { NitroEvent } from '@nitrots/nitro-renderer';
export class CatalogPurchaseFailureEvent extends CatalogEvent
export class CatalogPurchaseFailureEvent extends NitroEvent
{
public static PURCHASE_FAILED: string = 'CPFE_PURCHASE_FAILED';
private _code: number;
constructor(code: number)
{
super(CatalogEvent.PURCHASE_FAILED);
super(CatalogPurchaseFailureEvent.PURCHASE_FAILED);
this._code = code;
}

View File

@ -0,0 +1,20 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
export class CatalogPurchaseNotAllowedEvent extends NitroEvent
{
public static NOT_ALLOWED: string = 'CPNAE_NOT_ALLOWED';
private _code: number;
constructor(code: number)
{
super(CatalogPurchaseNotAllowedEvent.NOT_ALLOWED);
this._code = code;
}
public get code(): number
{
return this._code;
}
}

View File

@ -1,9 +1,11 @@
import { CatalogEvent } from './CatalogEvent';
import { NitroEvent } from '@nitrots/nitro-renderer';
export class CatalogPurchaseSoldOutEvent extends CatalogEvent
export class CatalogPurchaseSoldOutEvent extends NitroEvent
{
public static SOLD_OUT: string = 'CPSOE_SOLD_OUT';
constructor()
{
super(CatalogEvent.SOLD_OUT);
super(CatalogPurchaseSoldOutEvent.SOLD_OUT);
}
}

View File

@ -1,13 +1,14 @@
import { PurchaseOKMessageOfferData } from '@nitrots/nitro-renderer';
import { CatalogEvent } from './CatalogEvent';
import { NitroEvent, PurchaseOKMessageOfferData } from '@nitrots/nitro-renderer';
export class CatalogPurchasedEvent extends CatalogEvent
export class CatalogPurchasedEvent extends NitroEvent
{
public static PURCHASE_SUCCESS: string = 'CPE_PURCHASE_SUCCESS';
private _purchase: PurchaseOKMessageOfferData;
constructor(purchase: PurchaseOKMessageOfferData)
{
super(CatalogEvent.PURCHASE_SUCCESS);
super(CatalogPurchasedEvent.PURCHASE_SUCCESS);
this._purchase = purchase;
}

View File

@ -1,20 +0,0 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
import { IPurchasableOffer } from '../../components/catalog/common/IPurchasableOffer';
import { CatalogWidgetEvent } from './CatalogWidgetEvent';
export class CatalogSelectProductEvent extends NitroEvent
{
private _offer: IPurchasableOffer;
constructor(offer: IPurchasableOffer)
{
super(CatalogWidgetEvent.SELECT_PRODUCT);
this._offer = offer;
}
public get offer(): IPurchasableOffer
{
return this._offer;
}
}

View File

@ -1,10 +1,8 @@
export * from './CatalogEvent';
export * from './CatalogNameResultEvent';
export * from './CatalogPageOpenedEvent';
export * from './CatalogPageReadyEvent';
export * from './CatalogPurchasedEvent';
export * from './CatalogPurchaseFailureEvent';
export * from './CatalogPurchaseNotAllowedEvent';
export * from './CatalogPurchaseSoldOutEvent';
export * from './CatalogSelectProductEvent';
export * from './CatalogSetExtraPurchaseParameterEvent';
export * from './SetRoomPreviewerStuffDataEvent';