🆙 Init V3

This commit is contained in:
DuckieTM
2026-01-31 09:10:52 +01:00
commit 7feb10ab15
1733 changed files with 53405 additions and 0 deletions
@@ -0,0 +1,59 @@
import { MouseEventType } from '@nitrots/nitro-renderer';
import { FC, MouseEvent, useMemo, useState } from 'react';
import { IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api';
import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common';
import { useCatalog, useInventoryFurni } from '../../../../../hooks';
interface CatalogGridOfferViewProps extends LayoutGridItemProps
{
offer: IPurchasableOffer;
selectOffer: (offer: IPurchasableOffer) => void;
}
export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
{
const { offer = null, selectOffer = null, itemActive = false, ...rest } = props;
const [ isMouseDown, setMouseDown ] = useState(false);
const { requestOfferToMover = null } = useCatalog();
const { isVisible = false } = useInventoryFurni();
const iconUrl = useMemo(() =>
{
if(offer.pricingModel === Offer.PRICING_MODEL_BUNDLE)
{
return null;
}
return offer.product.getIconUrl(offer);
}, [ offer ]);
const onMouseEvent = (event: MouseEvent) =>
{
switch(event.type)
{
case MouseEventType.MOUSE_DOWN:
selectOffer(offer);
setMouseDown(true);
return;
case MouseEventType.MOUSE_UP:
setMouseDown(false);
return;
case MouseEventType.ROLL_OUT:
if(!isMouseDown || !itemActive || !isVisible) return;
requestOfferToMover(offer);
return;
}
};
const product = offer.product;
if(!product) return null;
return (
<LayoutGridItem itemActive={ itemActive } itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) } itemImage={ iconUrl } itemUniqueNumber={ product.uniqueLimitedItemSeriesSize } itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) } onMouseDown={ onMouseEvent } onMouseOut={ onMouseEvent } onMouseUp={ onMouseEvent } { ...rest }>
{ (offer.product.productType === ProductTypeEnum.ROBOT) &&
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> }
</LayoutGridItem>
);
};
@@ -0,0 +1,67 @@
import { RedeemVoucherMessageComposer, VoucherRedeemErrorMessageEvent, VoucherRedeemOkMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { FaTag } from 'react-icons/fa';
import { LocalizeText, SendMessageComposer } from '../../../../../api';
import { Button } from '../../../../../common';
import { useMessageEvent, useNotification } from '../../../../../hooks';
import { NitroInput } from '../../../../../layout';
export interface CatalogRedeemVoucherViewProps
{
text: string;
}
export const CatalogRedeemVoucherView: FC<CatalogRedeemVoucherViewProps> = props =>
{
const { text = null } = props;
const [ voucher, setVoucher ] = useState<string>('');
const [ isWaiting, setIsWaiting ] = useState(false);
const { simpleAlert = null } = useNotification();
const redeemVoucher = () =>
{
if(!voucher || !voucher.length || isWaiting) return;
SendMessageComposer(new RedeemVoucherMessageComposer(voucher));
setIsWaiting(true);
};
useMessageEvent<VoucherRedeemOkMessageEvent>(VoucherRedeemOkMessageEvent, event =>
{
const parser = event.getParser();
let message = LocalizeText('catalog.alert.voucherredeem.ok.description');
if(parser.productName) message = LocalizeText('catalog.alert.voucherredeem.ok.description.furni', [ 'productName', 'productDescription' ], [ parser.productName, parser.productDescription ]);
simpleAlert(message, null, null, null, LocalizeText('catalog.alert.voucherredeem.ok.title'));
setIsWaiting(false);
setVoucher('');
});
useMessageEvent<VoucherRedeemErrorMessageEvent>(VoucherRedeemErrorMessageEvent, event =>
{
const parser = event.getParser();
simpleAlert(LocalizeText(`catalog.alert.voucherredeem.error.description.${ parser.errorCode }`), null, null, null, LocalizeText('catalog.alert.voucherredeem.error.title'));
setIsWaiting(false);
});
return (
<div className="flex gap-1">
<NitroInput
placeholder={ text }
value={ voucher }
onChange={ event => setVoucher(event.target.value) } />
<Button disabled={ isWaiting } variant="primary" onClick={ redeemVoucher }>
<FaTag className="fa-icon" />
</Button>
</div>
);
};
@@ -0,0 +1,106 @@
import { GetSessionDataManager, IFurnitureData } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaSearch, FaTimes } from 'react-icons/fa';
import { CatalogPage, CatalogType, FilterCatalogNode, FurnitureOffer, GetOfferNodes, ICatalogNode, ICatalogPage, IPurchasableOffer, LocalizeText, PageLocalization, SearchResult } from '../../../../../api';
import { Button, Flex } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { NitroInput } from '../../../../../layout';
export const CatalogSearchView: FC<{}> = props =>
{
const [ searchValue, setSearchValue ] = useState('');
const { currentType = null, rootNode = null, offersToNodes = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalog();
useEffect(() =>
{
let search = searchValue?.toLocaleLowerCase().replace(' ', '');
if(!search || !search.length)
{
setSearchResult(null);
return;
}
const timeout = setTimeout(() =>
{
const furnitureDatas = GetSessionDataManager().getAllFurnitureData();
if(!furnitureDatas || !furnitureDatas.length) return;
const foundFurniture: IFurnitureData[] = [];
const foundFurniLines: string[] = [];
for(const furniture of furnitureDatas)
{
if((currentType === CatalogType.BUILDER) && !furniture.availableForBuildersClub) continue;
if((currentType === CatalogType.NORMAL) && furniture.excludeDynamic) continue;
const searchValues = [ furniture.className, furniture.name, furniture.description ].join(' ').replace(/ /gi, '').toLowerCase();
if((currentType === CatalogType.BUILDER) && (furniture.purchaseOfferId === -1) && (furniture.rentOfferId === -1))
{
if((furniture.furniLine !== '') && (foundFurniLines.indexOf(furniture.furniLine) < 0))
{
if(searchValues.indexOf(search) >= 0) foundFurniLines.push(furniture.furniLine);
}
}
else
{
const foundNodes = [
...GetOfferNodes(offersToNodes, furniture.purchaseOfferId),
...GetOfferNodes(offersToNodes, furniture.rentOfferId)
];
if(foundNodes.length)
{
if(searchValues.indexOf(search) >= 0) foundFurniture.push(furniture);
if(foundFurniture.length === 250) break;
}
}
}
const offers: IPurchasableOffer[] = [];
for(const furniture of foundFurniture) offers.push(new FurnitureOffer(furniture));
let nodes: ICatalogNode[] = [];
FilterCatalogNode(search, foundFurniLines, rootNode, nodes);
setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible))));
setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1) as ICatalogPage));
}, 300);
return () => clearTimeout(timeout);
}, [ offersToNodes, currentType, rootNode, searchValue, setCurrentPage, setSearchResult ]);
return (
<div className="flex gap-1">
<Flex fullWidth alignItems="center" position="relative">
<NitroInput
placeholder={ LocalizeText('generic.search') }
value={ searchValue }
onChange={ event => setSearchValue(event.target.value) } />
</Flex>
{ (!searchValue || !searchValue.length) &&
<Button className="catalog-search-button" variant="primary">
<FaSearch className="fa-icon" />
</Button> }
{ searchValue && !!searchValue.length &&
<Button className="catalog-search-button" variant="primary" onClick={ event => setSearchValue('') }>
<FaTimes className="fa-icon" />
</Button> }
</div>
);
};
@@ -0,0 +1,7 @@
import { ICatalogPage } from '../../../../../api';
export interface CatalogLayoutProps
{
page: ICatalogPage;
hideNavigation: () => void;
}
@@ -0,0 +1,54 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { Column, Grid, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
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 } = props;
const { currentOffer = null } = useCatalog();
return (
<>
<CatalogFirstProductSelectorWidgetView />
<Grid>
<Column overflow="hidden" size={ 7 }>
<CatalogItemGridWidgetView shrink />
<Column gap={ 1 } overflow="hidden">
<Text shrink truncate fontWeight="bold">{ LocalizeText('catalog_selectbadge') }</Text>
<CatalogBadgeSelectorWidgetView />
</Column>
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<div className="relative overflow-hidden">
<CatalogViewProductWidgetView />
</div>
<Column className="!flex-grow" gap={ 1 }>
<CatalogLimitedItemWidgetView />
<Text truncate className="!flex-grow">{ currentOffer.localizationName }</Text>
<div className="flex justify-end">
<CatalogTotalPriceWidget alignItems="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</>
);
};
@@ -0,0 +1,176 @@
import { ColorConverter } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { FaFillDrip } from 'react-icons/fa';
import { IPurchasableOffer } from '../../../../../api';
import { AutoGrid, Button, Column, Grid, LayoutGridItem, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
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 interface CatalogLayoutColorGroupViewProps extends CatalogLayoutProps
{
}
export const CatalogLayoutColorGroupingView: FC<CatalogLayoutColorGroupViewProps> = props =>
{
const { page = null } = props;
const [ colorableItems, setColorableItems ] = useState<Map<string, number[]>>(new Map<string, number[]>());
const { currentOffer = null, setCurrentOffer = null } = useCatalog();
const [ colorsShowing, setColorsShowing ] = useState<boolean>(false);
const sortByColorIndex = (a: IPurchasableOffer, b: IPurchasableOffer) =>
{
if(((!(a.product.furnitureData.colorIndex)) || (!(b.product.furnitureData.colorIndex))))
{
return 1;
}
if(a.product.furnitureData.colorIndex > b.product.furnitureData.colorIndex)
{
return 1;
}
if(a == b)
{
return 0;
}
return -1;
};
const sortyByFurnitureClassName = (a: IPurchasableOffer, b: IPurchasableOffer) =>
{
if(a.product.furnitureData.className > b.product.furnitureData.className)
{
return 1;
}
if(a == b)
{
return 0;
}
return -1;
};
const selectOffer = (offer: IPurchasableOffer) =>
{
offer.activate();
setCurrentOffer(offer);
};
const selectColor = (colorIndex: number, productName: string) =>
{
const fullName = `${ productName }*${ colorIndex }`;
const index = page.offers.findIndex(offer => offer.product.furnitureData.fullName === fullName);
if(index > -1)
{
selectOffer(page.offers[index]);
}
};
const offers = useMemo(() =>
{
const offers: IPurchasableOffer[] = [];
const addedColorableItems = new Map<string, boolean>();
const updatedColorableItems = new Map<string, number[]>();
page.offers.sort(sortByColorIndex);
page.offers.forEach(offer =>
{
if(!offer.product) return;
const furniData = offer.product.furnitureData;
if(!furniData || !furniData.hasIndexedColor)
{
offers.push(offer);
}
else
{
const name = furniData.className;
const colorIndex = furniData.colorIndex;
if(!updatedColorableItems.has(name))
{
updatedColorableItems.set(name, []);
}
let selectedColor = 0xFFFFFF;
if(furniData.colors)
{
for(let color of furniData.colors)
{
if(color !== 0xFFFFFF) // skip the white colors
{
selectedColor = color;
}
}
if(updatedColorableItems.get(name).indexOf(selectedColor) === -1)
{
updatedColorableItems.get(name)[colorIndex] = selectedColor;
}
}
if(!addedColorableItems.has(name))
{
offers.push(offer);
addedColorableItems.set(name, true);
}
}
});
offers.sort(sortyByFurnitureClassName);
setColorableItems(updatedColorableItems);
return offers;
}, [ page.offers ]);
return (
<Grid>
<Column overflow="hidden" size={ 7 }>
<AutoGrid columnCount={ 5 }>
{ (!colorsShowing || !currentOffer || !colorableItems.has(currentOffer.product.furnitureData.className)) &&
offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.product.furnitureData.hasIndexedColor ? currentOffer.product.furnitureData.className === offer.product.furnitureData.className : currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />)
}
{ (colorsShowing && currentOffer && colorableItems.has(currentOffer.product.furnitureData.className)) &&
colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (currentOffer.product.furnitureData.colorIndex === index) } itemColor={ ColorConverter.int2rgb(color) } onClick={ event => selectColor(index, currentOffer.product.furnitureData.className) } />)
}
</AutoGrid>
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<div className="relative overflow-hidden">
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" position="absolute" />
{ currentOffer.product.furnitureData.hasIndexedColor &&
<Button className="bottom-1 start-1" position="absolute" onClick={ event => setColorsShowing(prev => !prev) }>
<FaFillDrip className="fa-icon" />
</Button> }
</div>
<Column className="!flex-grow" gap={ 1 }>
<CatalogLimitedItemWidgetView />
<Text truncate className="!flex-grow">{ currentOffer.localizationName }</Text>
<div className="flex justify-between">
<div className="flex flex-col gap-1">
<CatalogSpinnerWidgetView />
</div>
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
);
};
@@ -0,0 +1,61 @@
import { FC } from 'react';
import { GetConfigurationValue, ProductTypeEnum } from '../../../../../api';
import { Column, Flex, Grid, LayoutImage, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
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 } = props;
const { currentOffer = null, currentPage = null } = useCatalog();
return (
<>
<Grid>
<Column overflow="hidden" size={ 7 }>
{ GetConfigurationValue('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
<CatalogItemGridWidgetView />
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) &&
<LayoutImage imageUrl={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Flex center overflow="hidden" style={ { height: 140 } }>
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" />
</> }
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
</Flex>
<Column grow gap={ 1 }>
<CatalogLimitedItemWidgetView />
<Text grow truncate>{ currentOffer.localizationName }</Text>
<div className="flex justify-between">
<div className="flex flex-col gap-1">
<CatalogSpinnerWidgetView />
</div>
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</>
);
};
@@ -0,0 +1,48 @@
import { FC } from 'react';
import { Column, Grid, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
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 } = props;
const { currentOffer = null } = useCatalog();
return (
<Grid>
<Column overflow="hidden" size={ 7 }>
<CatalogItemGridWidgetView />
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<div className="relative overflow-hidden">
<CatalogViewProductWidgetView />
<CatalogGuildBadgeWidgetView className="bottom-1 end-1" position="absolute" />
</div>
<Column grow gap={ 1 }>
<Text truncate>{ currentOffer.localizationName }</Text>
<div className="!flex-grow">
<CatalogGuildSelectorWidgetView />
</div>
<div className="flex justify-end">
<CatalogTotalPriceWidget alignItems="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
);
};
@@ -0,0 +1,49 @@
import { CatalogGroupsComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../../api';
import { Column, Grid, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayouGuildForumView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
const { currentOffer = null, setCurrentOffer = null, catalogOptions = null } = useCatalog();
const { groups = null } = catalogOptions;
useEffect(() =>
{
SendMessageComposer(new CatalogGroupsComposer());
}, [ page ]);
return (
<>
<CatalogFirstProductSelectorWidgetView />
<Grid>
<Column className="bg-muted rounded p-2 text-black" overflow="hidden" size={ 7 }>
<div className="overflow-auto" dangerouslySetInnerHTML={ { __html: page.localization.getText(1) } } />
</Column>
<Column gap={ 1 } overflow="hidden" size={ 5 }>
{ !!currentOffer &&
<>
<Column grow gap={ 1 }>
<Text truncate>{ currentOffer.localizationName }</Text>
<div className="!flex-grow">
<CatalogGuildSelectorWidgetView />
</div>
<div className="flex justify-end">
<CatalogTotalPriceWidget alignItems="end" />
</div>
<CatalogPurchaseWidgetView noGiftOption={ true } />
</Column>
</> }
</Column>
</Grid>
</>
);
};
@@ -0,0 +1,29 @@
import { CreateLinkEvent } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { Button } from '../../../../../common/Button';
import { Column } from '../../../../../common/Column';
import { Grid } from '../../../../../common/Grid';
import { LayoutImage } from '../../../../../common/layout/LayoutImage';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayouGuildFrontpageView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
return (
<Grid>
<Column className="bg-muted rounded p-2 text-black" overflow="hidden" size={ 7 }>
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } />
<div className="overflow-auto" dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(1) } } />
</Column>
<Column center overflow="hidden" size={ 5 }>
<LayoutImage imageUrl={ page.localization.getImage(1) } />
<Button onClick={ () => CreateLinkEvent('groups/create') }>
{ LocalizeText('catalog.start.guild.purchase.button') }
</Button>
</Column>
</Grid>
);
};
@@ -0,0 +1,15 @@
import { FC } from 'react';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutInfoLoyaltyView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
return (
<div className="h-full nitro-catalog-layout-info-loyalty text-black flex flex-row">
<div className="overflow-auto h-full flex flex-col info-loyalty-content">
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</div>
</div>
);
};
@@ -0,0 +1,8 @@
import { FC } from 'react';
import { CatalogLayoutProps } from './CatalogLayout.types';
import { CatalogLayoutPets3View } from './CatalogLayoutPets3View';
export const CatalogLayoutPets2View: FC<CatalogLayoutProps> = props =>
{
return <CatalogLayoutPets3View { ...props } />;
};
@@ -0,0 +1,25 @@
import { FC } from 'react';
import { Column } from '../../../../../common';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutPets3View: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const imageUrl = page.localization.getImage(1);
return (
<Column grow className="bg-muted rounded text-black p-2" overflow="hidden">
<div className="items-center gap-2">
{ imageUrl && <img alt="" src={ imageUrl } /> }
<div className="fs-5" dangerouslySetInnerHTML={ { __html: page.localization.getText(1) } } />
</div>
<Column grow alignItems="center" overflow="auto">
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } />
</Column>
<div className="flex items-center">
<div className="font-bold " dangerouslySetInnerHTML={ { __html: page.localization.getText(3) } } />
</div>
</Column>
);
};
@@ -0,0 +1,116 @@
import { GetRoomAdPurchaseInfoComposer, GetUserEventCatsMessageComposer, PurchaseRoomAdMessageComposer, RoomAdPurchaseInfoEvent, RoomEntryData } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { LocalizeText, SendMessageComposer } from '../../../../../api';
import { Button, Column, Text } from '../../../../../common';
import { useCatalog, useMessageEvent, useNavigator, useRoomPromote } from '../../../../../hooks';
import { NitroInput } from '../../../../../layout';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutRoomAdsView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const [ eventName, setEventName ] = useState<string>('');
const [ eventDesc, setEventDesc ] = useState<string>('');
const [ roomId, setRoomId ] = useState<number>(-1);
const [ availableRooms, setAvailableRooms ] = useState<RoomEntryData[]>([]);
const [ extended, setExtended ] = useState<boolean>(false);
const [ categoryId, setCategoryId ] = useState<number>(1);
const { categories = null } = useNavigator();
const { setIsVisible = null } = useCatalog();
const { promoteInformation, isExtended, setIsExtended } = useRoomPromote();
useEffect(() =>
{
if(isExtended)
{
setRoomId(promoteInformation.data.flatId);
setEventName(promoteInformation.data.eventName);
setEventDesc(promoteInformation.data.eventDescription);
setCategoryId(promoteInformation.data.categoryId);
setExtended(isExtended); // This is for sending to packet
setIsExtended(false); // This is from hook useRoomPromotte
}
}, [ isExtended, eventName, eventDesc, categoryId, promoteInformation.data, setIsExtended ]);
const resetData = () =>
{
setRoomId(-1);
setEventName('');
setEventDesc('');
setCategoryId(1);
setIsExtended(false);
setIsVisible(false);
};
const purchaseAd = () =>
{
const pageId = page.pageId;
const offerId = page.offers.length >= 1 ? page.offers[0].offerId : -1;
const flatId = roomId;
const name = eventName;
const desc = eventDesc;
const catId = categoryId;
SendMessageComposer(new PurchaseRoomAdMessageComposer(pageId, offerId, flatId, name, extended, desc, catId));
resetData();
};
useMessageEvent<RoomAdPurchaseInfoEvent>(RoomAdPurchaseInfoEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
setAvailableRooms(parser.rooms);
});
useEffect(() =>
{
SendMessageComposer(new GetRoomAdPurchaseInfoComposer());
// TODO: someone needs to fix this for morningstar
SendMessageComposer(new GetUserEventCatsMessageComposer());
}, []);
return (<>
<Text bold center>{ LocalizeText('roomad.catalog_header') }</Text>
<Column className="text-black" overflow="hidden" size={ 12 }>
<div>{ LocalizeText('roomad.catalog_text', [ 'duration' ], [ '120' ]) }</div>
<div className="p-1 rounded bg-muted">
<Column gap={ 2 }>
<Text bold>{ LocalizeText('navigator.category') }</Text>
<select className="form-select form-select-sm" disabled={ extended } value={ categoryId } onChange={ event => setCategoryId(parseInt(event.target.value)) }>
{ categories && categories.map((cat, index) => <option key={ index } value={ cat.id }>{ LocalizeText(cat.name) }</option>) }
</select>
</Column>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('roomad.catalog_name') }</Text>
<NitroInput maxLength={ 64 } readOnly={ extended } value={ eventName } onChange={ event => setEventName(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('roomad.catalog_description') }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" maxLength={ 64 } readOnly={ extended } value={ eventDesc } onChange={ event => setEventDesc(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('roomad.catalog_roomname') }</Text>
<select className="form-select form-select-sm" disabled={ extended } value={ roomId } onChange={ event => setRoomId(parseInt(event.target.value)) }>
<option disabled value={ -1 }>{ LocalizeText('roomad.catalog_roomname') }</option>
{ availableRooms && availableRooms.map((room, index) => <option key={ index } value={ room.roomId }>{ room.roomName }</option>) }
</select>
</div>
<div className="flex flex-col gap-1">
<Button disabled={ (!eventName || !eventDesc || roomId === -1) } variant={ (!eventName || !eventDesc || roomId === -1) ? 'danger' : 'success' } onClick={ purchaseAd }>{ extended ? LocalizeText('roomad.extend.event') : LocalizeText('buy') }</Button>
</div>
</div>
</Column>
</>
);
};
interface INavigatorCategory
{
id: number;
name: string;
visible: boolean;
}
@@ -0,0 +1,41 @@
import { FC } from 'react';
import { Column, Grid, Text } from '../../../../../common';
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 } = props;
return (
<>
<CatalogFirstProductSelectorWidgetView />
<Grid>
<Column overflow="hidden" size={ 7 }>
{ !!page.localization.getText(2) &&
<Text dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } /> }
<Column grow className="bg-muted p-2 rounded" overflow="hidden">
<CatalogBundleGridWidgetView fullWidth className="nitro-catalog-layout-bundle-grid" />
</Column>
</Column>
<Column gap={ 1 } overflow="hidden" size={ 5 }>
{ !!page.localization.getText(1) &&
<Text center small overflow="auto">{ page.localization.getText(1) }</Text> }
<Column grow gap={ 0 } overflow="hidden" position="relative">
{ !!page.localization.getImage(1) &&
<img alt="" className="!flex-grow" src={ page.localization.getImage(1) } /> }
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-0 start-0" position="absolute" />
<CatalogSimplePriceWidgetView />
</Column>
<div className="flex flex-col gap-1">
<CatalogPurchaseWidgetView />
</div>
</Column>
</Grid>
</>
);
};
@@ -0,0 +1,41 @@
import { FC } from 'react';
import { Column, Grid, Text } from '../../../../../common';
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 CatalogLayoutSingleBundleView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
return (
<>
<CatalogFirstProductSelectorWidgetView />
<Grid>
<Column overflow="hidden" size={ 7 }>
{ !!page.localization.getText(2) &&
<Text dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } /> }
<Column grow className="bg-muted p-2 rounded" overflow="hidden">
<CatalogBundleGridWidgetView fullWidth className="nitro-catalog-layout-bundle-grid" />
</Column>
</Column>
<Column gap={ 1 } overflow="hidden" size={ 5 }>
{ !!page.localization.getText(1) &&
<Text center small overflow="auto">{ page.localization.getText(1) }</Text> }
<Column grow gap={ 0 } overflow="hidden" position="relative">
{ !!page.localization.getImage(1) &&
<img alt="" className="!flex-grow" src={ page.localization.getImage(1) } /> }
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-0 start-0" position="absolute" />
<CatalogSimplePriceWidgetView />
</Column>
<div className="flex flex-col gap-1">
<CatalogPurchaseWidgetView />
</div>
</Column>
</Grid>
</>
);
};
@@ -0,0 +1,113 @@
import { GetOfficialSongIdMessageComposer, GetSoundManager, MusicPriorities, OfficialSongIdMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../../api';
import { Button, Column, Grid, LayoutImage, Text } from '../../../../../common';
import { useCatalog, useMessageEvent } from '../../../../../hooks';
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
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 CatalogLayoutSoundMachineView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const [ songId, setSongId ] = useState(-1);
const [ officialSongId, setOfficialSongId ] = useState('');
const { currentOffer = null, currentPage = null } = useCatalog();
const previewSong = (previewSongId: number) => GetSoundManager().musicController?.playSong(previewSongId, MusicPriorities.PRIORITY_PURCHASE_PREVIEW, 15, 0, 0, 0);
useMessageEvent<OfficialSongIdMessageEvent>(OfficialSongIdMessageEvent, event =>
{
const parser = event.getParser();
if(parser.officialSongId !== officialSongId) return;
setSongId(parser.songId);
});
useEffect(() =>
{
if(!currentOffer) return;
const product = currentOffer.product;
if(!product) return;
if(product.extraParam.length > 0)
{
const id = parseInt(product.extraParam);
if(id > 0)
{
setSongId(id);
}
else
{
setOfficialSongId(product.extraParam);
SendMessageComposer(new GetOfficialSongIdMessageComposer(product.extraParam));
}
}
else
{
setOfficialSongId('');
setSongId(-1);
}
return () => GetSoundManager().musicController?.stop(MusicPriorities.PRIORITY_PURCHASE_PREVIEW);
}, [ currentOffer ]);
useEffect(() =>
{
return () => GetSoundManager().musicController?.stop(MusicPriorities.PRIORITY_PURCHASE_PREVIEW);
}, []);
return (
<>
<Grid>
<Column overflow="hidden" size={ 7 }>
{ GetConfigurationValue('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
<CatalogItemGridWidgetView />
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) &&
<LayoutImage imageUrl={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<div className="flex items-center justify-center overflow-hidden" style={ { height: 140 } }>
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" />
</> }
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
</div>
<Column grow gap={ 1 }>
<CatalogLimitedItemWidgetView />
<Text grow truncate>{ currentOffer.localizationName }</Text>
{ songId > -1 && <Button onClick={ () => previewSong(songId) }>{ LocalizeText('play_preview_button') }</Button>
}
<div className="flex justify-between">
<div className="flex flex-col gap-1">
<CatalogSpinnerWidgetView />
</div>
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</>
);
};
@@ -0,0 +1,47 @@
import { FC, useEffect } from 'react';
import { Column, Grid, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
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 } = props;
const { currentOffer = null, roomPreviewer = null } = useCatalog();
useEffect(() =>
{
roomPreviewer.updatePreviewObjectBoundingRectangle();
}, [ roomPreviewer ]);
return (
<Grid>
<Column overflow="hidden" size={ 7 }>
<CatalogSpacesWidgetView />
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<div className="relative overflow-hidden">
<CatalogViewProductWidgetView />
</div>
<Column grow gap={ 1 }>
<Text grow truncate>{ currentOffer.localizationName }</Text>
<div className="flex justify-end">
<CatalogTotalPriceWidget alignItems="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
);
};
@@ -0,0 +1,56 @@
import { FC, useEffect, useState } from 'react';
import { Column, Grid, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
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 } = props;
const [ trophyText, setTrophyText ] = useState<string>('');
const { currentOffer = null, setPurchaseOptions = null } = useCatalog();
useEffect(() =>
{
if(!currentOffer) return;
setPurchaseOptions(prevValue =>
{
const newValue = { ...prevValue };
newValue.extraData = trophyText;
return newValue;
});
}, [ currentOffer, trophyText, setPurchaseOptions ]);
return (
<Grid>
<Column overflow="hidden" size={ 7 }>
<CatalogItemGridWidgetView />
<textarea className="!flex-grow form-control w-full" defaultValue={ trophyText || '' } onChange={ event => setTrophyText(event.target.value) } />
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !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>
<div className="flex justify-end">
<CatalogTotalPriceWidget alignItems="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
);
};
@@ -0,0 +1,191 @@
import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { CatalogPurchaseState, LocalizeText, SendMessageComposer } from '../../../../../api';
import { AutoGrid, Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common';
import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events';
import { useCatalog, usePurse, useUiEvent } from '../../../../../hooks';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
{
const [ pendingOffer, setPendingOffer ] = useState<ClubOfferData>(null);
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
const { currentPage = null, catalogOptions = null } = useCatalog();
const { purse = null, getCurrencyAmount = null } = usePurse();
const { clubOffers = null } = catalogOptions;
const onCatalogEvent = useCallback((event: CatalogEvent) =>
{
switch(event.type)
{
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
setPurchaseState(CatalogPurchaseState.NONE);
return;
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
setPurchaseState(CatalogPurchaseState.FAILED);
return;
}
}, []);
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
const getOfferText = useCallback((offer: ClubOfferData) =>
{
let offerText = '';
if(offer.months > 0)
{
offerText = LocalizeText('catalog.vip.item.header.months', [ 'num_months' ], [ offer.months.toString() ]);
}
if(offer.extraDays > 0)
{
if(offerText !== '') offerText += ' ';
offerText += (' ' + LocalizeText('catalog.vip.item.header.days', [ 'num_days' ], [ offer.extraDays.toString() ]));
}
return offerText;
}, []);
const getPurchaseHeader = useCallback(() =>
{
if(!purse) return '';
const extensionOrSubscription = (purse.clubDays > 0 || purse.clubPeriods > 0) ? 'extension.' : 'subscription.';
const daysOrMonths = ((pendingOffer.months === 0) ? 'days' : 'months');
const daysOrMonthsText = ((pendingOffer.months === 0) ? pendingOffer.extraDays : pendingOffer.months);
const locale = LocalizeText('catalog.vip.buy.confirm.' + extensionOrSubscription + daysOrMonths);
return locale.replace('%NUM_' + daysOrMonths.toUpperCase() + '%', daysOrMonthsText.toString());
}, [ pendingOffer, purse ]);
const getPurchaseValidUntil = useCallback(() =>
{
let locale = LocalizeText('catalog.vip.buy.confirm.end_date');
locale = locale.replace('%month%', pendingOffer.month.toString());
locale = locale.replace('%day%', pendingOffer.day.toString());
locale = locale.replace('%year%', pendingOffer.year.toString());
return locale;
}, [ pendingOffer ]);
const getSubscriptionDetails = useMemo(() =>
{
const clubDays = purse.clubDays;
const clubPeriods = purse.clubPeriods;
const totalDays = (clubPeriods * 31) + clubDays;
return LocalizeText('catalog.vip.extend.info', [ 'days' ], [ totalDays.toString() ]);
}, [ purse ]);
const purchaseSubscription = useCallback(() =>
{
if(!pendingOffer) return;
setPurchaseState(CatalogPurchaseState.PURCHASE);
SendMessageComposer(new PurchaseFromCatalogComposer(currentPage.pageId, pendingOffer.offerId, null, 1));
}, [ pendingOffer, currentPage ]);
const setOffer = useCallback((offer: ClubOfferData) =>
{
setPurchaseState(CatalogPurchaseState.NONE);
setPendingOffer(offer);
}, []);
const getPurchaseButton = useCallback(() =>
{
if(!pendingOffer) return null;
if(pendingOffer.priceCredits > getCurrencyAmount(-1))
{
return <Button fullWidth variant="danger">{ LocalizeText('catalog.alert.notenough.title') }</Button>;
}
if(pendingOffer.priceActivityPoints > getCurrencyAmount(pendingOffer.priceActivityPointsType))
{
return <Button fullWidth variant="danger">{ LocalizeText('catalog.alert.notenough.activitypoints.title.' + pendingOffer.priceActivityPointsType) }</Button>;
}
switch(purchaseState)
{
case CatalogPurchaseState.CONFIRM:
return <Button fullWidth variant="warning" onClick={ purchaseSubscription }>{ LocalizeText('catalog.marketplace.confirm_title') }</Button>;
case CatalogPurchaseState.PURCHASE:
return <Button disabled fullWidth variant="primary"><LayoutLoadingSpinnerView /></Button>;
case CatalogPurchaseState.FAILED:
return <Button disabled fullWidth variant="danger">{ LocalizeText('generic.failed') }</Button>;
case CatalogPurchaseState.NONE:
default:
return <Button fullWidth variant="success" onClick={ () => setPurchaseState(CatalogPurchaseState.CONFIRM) }>{ LocalizeText('buy') }</Button>;
}
}, [ pendingOffer, purchaseState, purchaseSubscription, getCurrencyAmount ]);
useEffect(() =>
{
if(!clubOffers) SendMessageComposer(new GetClubOffersMessageComposer(1));
}, [ clubOffers ]);
return (
<Grid>
<Column fullHeight justifyContent="between" overflow="hidden" size={ 7 }>
<AutoGrid className="nitro-catalog-layout-vip-buy-grid" columnCount={ 1 }>
{ clubOffers && (clubOffers.length > 0) && clubOffers.map((offer, index) =>
{
return (
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-1" column={ false } itemActive={ pendingOffer === offer } justifyContent="between" onClick={ () => setOffer(offer) }>
<i className="icon-hc-banner" />
<Column gap={ 0 } justifyContent="end">
<Text textEnd>{ getOfferText(offer) }</Text>
<Flex gap={ 1 } justifyContent="end">
{ (offer.priceCredits > 0) &&
<Flex alignItems="center" gap={ 1 } justifyContent="end">
<Text>{ offer.priceCredits }</Text>
<LayoutCurrencyIcon type={ -1 } />
</Flex> }
{ (offer.priceActivityPoints > 0) &&
<Flex alignItems="center" gap={ 1 } justifyContent="end">
<Text>{ offer.priceActivityPoints }</Text>
<LayoutCurrencyIcon type={ offer.priceActivityPointsType } />
</Flex> }
</Flex>
</Column>
</LayoutGridItem>
);
}) }
</AutoGrid>
<Text center dangerouslySetInnerHTML={ { __html: LocalizeText('catalog.vip.buy.hccenter') } }></Text>
</Column>
<Column overflow="hidden" size={ 5 }>
<Column center fullHeight overflow="hidden">
{ currentPage.localization.getImage(1) && <img alt="" src={ currentPage.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: getSubscriptionDetails } } overflow="auto" />
</Column>
{ pendingOffer &&
<Column fullWidth grow justifyContent="end">
<Flex alignItems="end">
<Column grow gap={ 0 }>
<Text fontWeight="bold">{ getPurchaseHeader() }</Text>
<Text>{ getPurchaseValidUntil() }</Text>
</Column>
<div className="flex flex-col gap-1">
{ (pendingOffer.priceCredits > 0) &&
<Flex alignItems="center" gap={ 1 } justifyContent="end">
<Text>{ pendingOffer.priceCredits }</Text>
<LayoutCurrencyIcon type={ -1 } />
</Flex> }
{ (pendingOffer.priceActivityPoints > 0) &&
<Flex alignItems="center" gap={ 1 } justifyContent="end">
<Text>{ pendingOffer.priceActivityPoints }</Text>
<LayoutCurrencyIcon type={ pendingOffer.priceActivityPointsType } />
</Flex> }
</div>
</Flex>
{ getPurchaseButton() }
</Column> }
</Column>
</Grid>
);
};
@@ -0,0 +1,80 @@
import { ICatalogPage } from '../../../../../api';
import { CatalogLayoutProps } from './CatalogLayout.types';
import { CatalogLayoutBadgeDisplayView } from './CatalogLayoutBadgeDisplayView';
import { CatalogLayoutColorGroupingView } from './CatalogLayoutColorGroupingView';
import { CatalogLayoutDefaultView } from './CatalogLayoutDefaultView';
import { CatalogLayouGuildCustomFurniView } from './CatalogLayoutGuildCustomFurniView';
import { CatalogLayouGuildForumView } from './CatalogLayoutGuildForumView';
import { CatalogLayouGuildFrontpageView } from './CatalogLayoutGuildFrontpageView';
import { CatalogLayoutInfoLoyaltyView } from './CatalogLayoutInfoLoyaltyView';
import { CatalogLayoutPets2View } from './CatalogLayoutPets2View';
import { CatalogLayoutPets3View } from './CatalogLayoutPets3View';
import { CatalogLayoutRoomAdsView } from './CatalogLayoutRoomAdsView';
import { CatalogLayoutRoomBundleView } from './CatalogLayoutRoomBundleView';
import { CatalogLayoutSingleBundleView } from './CatalogLayoutSingleBundleView';
import { CatalogLayoutSoundMachineView } from './CatalogLayoutSoundMachineView';
import { CatalogLayoutSpacesView } from './CatalogLayoutSpacesView';
import { CatalogLayoutTrophiesView } from './CatalogLayoutTrophiesView';
import { CatalogLayoutVipBuyView } from './CatalogLayoutVipBuyView';
import { CatalogLayoutFrontpage4View } from './frontpage4/CatalogLayoutFrontpage4View';
import { CatalogLayoutMarketplaceOwnItemsView } from './marketplace/CatalogLayoutMarketplaceOwnItemsView';
import { CatalogLayoutMarketplacePublicItemsView } from './marketplace/CatalogLayoutMarketplacePublicItemsView';
import { CatalogLayoutPetView } from './pets/CatalogLayoutPetView';
import { CatalogLayoutVipGiftsView } from './vip-gifts/CatalogLayoutVipGiftsView';
export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void) =>
{
if(!page) return null;
const layoutProps: CatalogLayoutProps = { page, hideNavigation };
switch(page.layoutCode)
{
case 'frontpage_featured':
return null;
case 'frontpage4':
return <CatalogLayoutFrontpage4View { ...layoutProps } />;
case 'pets':
return <CatalogLayoutPetView { ...layoutProps } />;
case 'pets2':
return <CatalogLayoutPets2View { ...layoutProps } />;
case 'pets3':
return <CatalogLayoutPets3View { ...layoutProps } />;
case 'vip_buy':
return <CatalogLayoutVipBuyView { ...layoutProps } />;
case 'guild_frontpage':
return <CatalogLayouGuildFrontpageView { ...layoutProps } />;
case 'guild_forum':
return <CatalogLayouGuildForumView { ...layoutProps } />;
case 'guild_custom_furni':
return <CatalogLayouGuildCustomFurniView { ...layoutProps } />;
case 'club_gifts':
return <CatalogLayoutVipGiftsView { ...layoutProps } />;
case 'marketplace_own_items':
return <CatalogLayoutMarketplaceOwnItemsView { ...layoutProps } />;
case 'marketplace':
return <CatalogLayoutMarketplacePublicItemsView { ...layoutProps } />;
case 'single_bundle':
return <CatalogLayoutSingleBundleView { ...layoutProps } />;
case 'room_bundle':
return <CatalogLayoutRoomBundleView { ...layoutProps } />;
case 'spaces_new':
return <CatalogLayoutSpacesView { ...layoutProps } />;
case 'trophies':
return <CatalogLayoutTrophiesView { ...layoutProps } />;
case 'info_loyalty':
return <CatalogLayoutInfoLoyaltyView { ...layoutProps } />;
case 'badge_display':
return <CatalogLayoutBadgeDisplayView { ...layoutProps } />;
case 'roomads':
return <CatalogLayoutRoomAdsView { ...layoutProps } />;
case 'default_3x3_color_grouping':
return <CatalogLayoutColorGroupingView { ...layoutProps } />;
case 'soundmachine':
return <CatalogLayoutSoundMachineView { ...layoutProps } />;
case 'bots':
case 'default_3x3':
default:
return <CatalogLayoutDefaultView { ...layoutProps } />;
}
};
@@ -0,0 +1,37 @@
import { FrontPageItem } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { GetConfigurationValue } from '../../../../../../api';
import { LayoutBackgroundImage, LayoutBackgroundImageProps } from '../../../../../../common';
import { Text } from '../../../../../../common/Text';
export interface CatalogLayoutFrontPageItemViewProps extends LayoutBackgroundImageProps
{
item: FrontPageItem;
}
export const CatalogLayoutFrontPageItemView: FC<CatalogLayoutFrontPageItemViewProps> = props =>
{
const { item = null, position = 'relative', pointer = true, overflow = 'hidden', fullHeight = true, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'rounded', 'nitro-front-page-item' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
if(!item) return null;
const imageUrl = (GetConfigurationValue<string>('image.library.url') + item.itemPromoImage);
return (
<LayoutBackgroundImage classNames={ getClassNames } fullHeight={ fullHeight } imageUrl={ imageUrl } overflow={ overflow } pointer={ pointer } position={ position } { ...rest }>
<Text className="bg-dark rounded p-2 m-2 bottom-0" position="absolute" variant="white">
{ item.itemName }
</Text>
{ children }
</LayoutBackgroundImage>
);
};
@@ -0,0 +1,49 @@
import { CreateLinkEvent, FrontPageItem } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect } from 'react';
import { Column, Grid } from '../../../../../../common';
import { useCatalog } from '../../../../../../hooks';
import { CatalogRedeemVoucherView } from '../../common/CatalogRedeemVoucherView';
import { CatalogLayoutProps } from '../CatalogLayout.types';
import { CatalogLayoutFrontPageItemView } from './CatalogLayoutFrontPageItemView';
export const CatalogLayoutFrontpage4View: FC<CatalogLayoutProps> = props =>
{
const { page = null, hideNavigation = null } = props;
const { frontPageItems = [] } = useCatalog();
const selectItem = useCallback((item: FrontPageItem) =>
{
switch(item.type)
{
case FrontPageItem.ITEM_CATALOGUE_PAGE:
CreateLinkEvent(`catalog/open/${ item.catalogPageLocation }`);
return;
case FrontPageItem.ITEM_PRODUCT_OFFER:
CreateLinkEvent(`catalog/open/${ item.productOfferId }`);
return;
}
}, []);
useEffect(() =>
{
hideNavigation();
}, [ page, hideNavigation ]);
return (
<Grid>
<Column size={ 4 }>
{ frontPageItems[0] &&
<CatalogLayoutFrontPageItemView item={ frontPageItems[0] } onClick={ event => selectItem(frontPageItems[0]) } /> }
</Column>
<Column size={ 8 }>
{ frontPageItems[1] &&
<CatalogLayoutFrontPageItemView item={ frontPageItems[1] } onClick={ event => selectItem(frontPageItems[1]) } /> }
{ frontPageItems[2] &&
<CatalogLayoutFrontPageItemView item={ frontPageItems[2] } onClick={ event => selectItem(frontPageItems[2]) } /> }
{ frontPageItems[3] &&
<CatalogLayoutFrontPageItemView item={ frontPageItems[3] } onClick={ event => selectItem(frontPageItems[3]) } /> }
<CatalogRedeemVoucherView text={ page.localization.getText(1) } />
</Column>
</Grid>
);
};
@@ -0,0 +1,83 @@
import { FC, useCallback, useMemo } from 'react';
import { GetImageIconUrlForProduct, LocalizeText, MarketPlaceOfferState, MarketplaceOfferData, ProductTypeEnum } from '../../../../../../api';
import { Button, Column, LayoutGridItem, Text } from '../../../../../../common';
export interface MarketplaceItemViewProps
{
offerData: MarketplaceOfferData;
type?: number;
onClick(offerData: MarketplaceOfferData): void;
}
export const OWN_OFFER = 1;
export const PUBLIC_OFFER = 2;
export const CatalogLayoutMarketplaceItemView: FC<MarketplaceItemViewProps> = props =>
{
const { offerData = null, type = PUBLIC_OFFER, onClick = null } = props;
const getMarketplaceOfferTitle = useMemo(() =>
{
if(!offerData) return '';
// desc
return LocalizeText(((offerData.furniType === 2) ? 'wallItem' : 'roomItem') + `.name.${ offerData.furniId }`);
}, [ offerData ]);
const offerTime = useCallback( () =>
{
if(!offerData) return '';
if(offerData.status === MarketPlaceOfferState.SOLD) return LocalizeText('catalog.marketplace.offer.sold');
if(offerData.timeLeftMinutes <= 0) return LocalizeText('catalog.marketplace.offer.expired');
const time = Math.max(1, offerData.timeLeftMinutes);
const hours = Math.floor(time / 60);
const minutes = time - (hours * 60);
let text = minutes + ' ' + LocalizeText('catalog.marketplace.offer.minutes');
if(hours > 0)
{
text = hours + ' ' + LocalizeText('catalog.marketplace.offer.hours') + ' ' + text;
}
return LocalizeText('catalog.marketplace.offer.time_left', [ 'time' ], [ text ] );
}, [ offerData ]);
return (
<LayoutGridItem shrink alignItems="center" center={ false } className="p-1" column={ false }>
<Column style={ { width: 40, height: 40 } }>
<LayoutGridItem column={ false } itemImage={ GetImageIconUrlForProduct(((offerData.furniType === MarketplaceOfferData.TYPE_FLOOR) ? ProductTypeEnum.FLOOR : ProductTypeEnum.WALL), offerData.furniId, offerData.extraData) } itemUniqueNumber={ offerData.isUniqueLimitedItem ? offerData.stuffData.uniqueNumber : 0 } />
</Column>
<Column grow gap={ 0 }>
<Text fontWeight="bold">{ getMarketplaceOfferTitle }</Text>
{ (type === OWN_OFFER) &&
<>
<Text>{ LocalizeText('catalog.marketplace.offer.price_own_item', [ 'price' ], [ offerData.price.toString() ]) }</Text>
<Text>{ offerTime() }</Text>
</> }
{ (type === PUBLIC_OFFER) &&
<>
<Text>{ LocalizeText('catalog.marketplace.offer.price_public_item', [ 'price', 'average' ], [ offerData.price.toString(), ((offerData.averagePrice > 0) ? offerData.averagePrice.toString() : '-') ]) }</Text>
<Text>{ LocalizeText('catalog.marketplace.offer_count', [ 'count' ], [ offerData.offerCount.toString() ]) }</Text>
</> }
</Column>
<div className="flex flex-col gap-1">
{ ((type === OWN_OFFER) && (offerData.status !== MarketPlaceOfferState.SOLD)) &&
<Button variant="secondary" onClick={ () => onClick(offerData) }>
{ LocalizeText('catalog.marketplace.offer.pick') }
</Button> }
{ type === PUBLIC_OFFER &&
<>
<Button variant="secondary" onClick={ () => onClick(offerData) }>
{ LocalizeText('buy') }
</Button>
<Button disabled variant="secondary">
{ LocalizeText('catalog.marketplace.view_more') }
</Button>
</> }
</div>
</LayoutGridItem>
);
};
@@ -0,0 +1,102 @@
import { CancelMarketplaceOfferMessageComposer, GetMarketplaceOwnOffersMessageComposer, MarketplaceCancelOfferResultEvent, MarketplaceOwnOffersEvent, RedeemMarketplaceOfferCreditsMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { LocalizeText, MarketplaceOfferData, MarketPlaceOfferState, NotificationAlertType, SendMessageComposer } from '../../../../../../api';
import { Button, Column, Text } from '../../../../../../common';
import { useMessageEvent, useNotification } from '../../../../../../hooks';
import { CatalogLayoutProps } from '../CatalogLayout.types';
import { CatalogLayoutMarketplaceItemView, OWN_OFFER } from './CatalogLayoutMarketplaceItemView';
export const CatalogLayoutMarketplaceOwnItemsView: FC<CatalogLayoutProps> = props =>
{
const [ creditsWaiting, setCreditsWaiting ] = useState(0);
const [ offers, setOffers ] = useState<MarketplaceOfferData[]>([]);
const { simpleAlert = null } = useNotification();
useMessageEvent<MarketplaceOwnOffersEvent>(MarketplaceOwnOffersEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
const offers = parser.offers.map(offer =>
{
const newOffer = new MarketplaceOfferData(offer.offerId, offer.furniId, offer.furniType, offer.extraData, offer.stuffData, offer.price, offer.status, offer.averagePrice, offer.offerCount);
newOffer.timeLeftMinutes = offer.timeLeftMinutes;
return newOffer;
});
setCreditsWaiting(parser.creditsWaiting);
setOffers(offers);
});
useMessageEvent<MarketplaceCancelOfferResultEvent>(MarketplaceCancelOfferResultEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
if(!parser.success)
{
simpleAlert(LocalizeText('catalog.marketplace.cancel_failed'), NotificationAlertType.DEFAULT, null, null, LocalizeText('catalog.marketplace.operation_failed.topic'));
return;
}
setOffers(prevValue => prevValue.filter(value => (value.offerId !== parser.offerId)));
});
const soldOffers = useMemo(() =>
{
return offers.filter(value => (value.status === MarketPlaceOfferState.SOLD));
}, [ offers ]);
const redeemSoldOffers = useCallback(() =>
{
setOffers(prevValue =>
{
const idsToDelete = soldOffers.map(value => value.offerId);
return prevValue.filter(value => (idsToDelete.indexOf(value.offerId) === -1));
});
SendMessageComposer(new RedeemMarketplaceOfferCreditsMessageComposer());
}, [ soldOffers ]);
const takeItemBack = (offerData: MarketplaceOfferData) =>
{
SendMessageComposer(new CancelMarketplaceOfferMessageComposer(offerData.offerId));
};
useEffect(() =>
{
SendMessageComposer(new GetMarketplaceOwnOffersMessageComposer());
}, []);
return (
<Column overflow="hidden">
{ (creditsWaiting <= 0) &&
<Text center className="bg-muted rounded p-1">
{ LocalizeText('catalog.marketplace.redeem.no_sold_items') }
</Text> }
{ (creditsWaiting > 0) &&
<Column center className="bg-muted rounded p-2" gap={ 1 }>
<Text>
{ LocalizeText('catalog.marketplace.redeem.get_credits', [ 'count', 'credits' ], [ soldOffers.length.toString(), creditsWaiting.toString() ]) }
</Text>
<Button className="mt-1" onClick={ redeemSoldOffers }>
{ LocalizeText('catalog.marketplace.offer.redeem') }
</Button>
</Column> }
<Column gap={ 1 } overflow="hidden">
<Text shrink truncate fontWeight="bold">
{ LocalizeText('catalog.marketplace.items_found', [ 'count' ], [ offers.length.toString() ]) }
</Text>
<Column className="nitro-catalog-layout-marketplace-grid" overflow="auto">
{ (offers.length > 0) && offers.map(offer => <CatalogLayoutMarketplaceItemView key={ offer.offerId } offerData={ offer } type={ OWN_OFFER } onClick={ takeItemBack } />) }
</Column>
</Column>
</Column>
);
};
@@ -0,0 +1,161 @@
import { BuyMarketplaceOfferMessageComposer, GetMarketplaceOffersMessageComposer, MarketplaceBuyOfferResultEvent, MarketPlaceOffersEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react';
import { IMarketplaceSearchOptions, LocalizeText, MarketplaceOfferData, MarketplaceSearchType, NotificationAlertType, SendMessageComposer } from '../../../../../../api';
import { Button, Column, Text } from '../../../../../../common';
import { useMessageEvent, useNotification, usePurse } from '../../../../../../hooks';
import { CatalogLayoutProps } from '../CatalogLayout.types';
import { CatalogLayoutMarketplaceItemView, PUBLIC_OFFER } from './CatalogLayoutMarketplaceItemView';
import { SearchFormView } from './CatalogLayoutMarketplaceSearchFormView';
const SORT_TYPES_VALUE = [ 1, 2 ];
const SORT_TYPES_ACTIVITY = [ 3, 4, 5, 6 ];
const SORT_TYPES_ADVANCED = [ 1, 2, 3, 4, 5, 6 ];
export interface CatalogLayoutMarketplacePublicItemsViewProps extends CatalogLayoutProps
{
}
export const CatalogLayoutMarketplacePublicItemsView: FC<CatalogLayoutMarketplacePublicItemsViewProps> = props =>
{
const [ searchType, setSearchType ] = useState(MarketplaceSearchType.BY_ACTIVITY);
const [ totalItemsFound, setTotalItemsFound ] = useState(0);
const [ offers, setOffers ] = useState(new Map<number, MarketplaceOfferData>());
const [ lastSearch, setLastSearch ] = useState<IMarketplaceSearchOptions>({ minPrice: -1, maxPrice: -1, query: '', type: 3 });
const { getCurrencyAmount = null } = usePurse();
const { simpleAlert = null, showConfirm = null } = useNotification();
const requestOffers = useCallback((options: IMarketplaceSearchOptions) =>
{
setLastSearch(options);
SendMessageComposer(new GetMarketplaceOffersMessageComposer(options.minPrice, options.maxPrice, options.query, options.type));
}, []);
const getSortTypes = useMemo(() =>
{
switch(searchType)
{
case MarketplaceSearchType.BY_ACTIVITY:
return SORT_TYPES_ACTIVITY;
case MarketplaceSearchType.BY_VALUE:
return SORT_TYPES_VALUE;
case MarketplaceSearchType.ADVANCED:
return SORT_TYPES_ADVANCED;
}
return [];
}, [ searchType ]);
const purchaseItem = useCallback((offerData: MarketplaceOfferData) =>
{
if(offerData.price > getCurrencyAmount(-1))
{
simpleAlert(LocalizeText('catalog.alert.notenough.credits.description'), NotificationAlertType.DEFAULT, null, null, LocalizeText('catalog.alert.notenough.title'));
return;
}
const offerId = offerData.offerId;
showConfirm(LocalizeText('catalog.marketplace.confirm_header'), () =>
{
SendMessageComposer(new BuyMarketplaceOfferMessageComposer(offerId));
},
null, null, null, LocalizeText('catalog.marketplace.confirm_title'));
}, [ getCurrencyAmount, simpleAlert, showConfirm ]);
useMessageEvent<MarketPlaceOffersEvent>(MarketPlaceOffersEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
const latestOffers = new Map<number, MarketplaceOfferData>();
parser.offers.forEach(entry =>
{
const offerEntry = new MarketplaceOfferData(entry.offerId, entry.furniId, entry.furniType, entry.extraData, entry.stuffData, entry.price, entry.status, entry.averagePrice, entry.offerCount);
offerEntry.timeLeftMinutes = entry.timeLeftMinutes;
latestOffers.set(entry.offerId, offerEntry);
});
setTotalItemsFound(parser.totalItemsFound);
setOffers(latestOffers);
});
useMessageEvent<MarketplaceBuyOfferResultEvent>(MarketplaceBuyOfferResultEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
switch(parser.result)
{
case 1:
requestOffers(lastSearch);
break;
case 2:
setOffers(prev =>
{
const newVal = new Map(prev);
newVal.delete(parser.requestedOfferId);
return newVal;
});
simpleAlert(LocalizeText('catalog.marketplace.not_available_header'), NotificationAlertType.DEFAULT, null, null, LocalizeText('catalog.marketplace.not_available_title'));
break;
case 3:
// our shit was updated
// todo: some dialogue modal
setOffers(prev =>
{
const newVal = new Map(prev);
const item = newVal.get(parser.requestedOfferId);
if(item)
{
item.offerId = parser.offerId;
item.price = parser.newPrice;
item.offerCount--;
newVal.set(item.offerId, item);
}
newVal.delete(parser.requestedOfferId);
return newVal;
});
showConfirm(LocalizeText('catalog.marketplace.confirm_higher_header') +
'\n' + LocalizeText('catalog.marketplace.confirm_price', [ 'price' ], [ parser.newPrice.toString() ]), () =>
{
SendMessageComposer(new BuyMarketplaceOfferMessageComposer(parser.offerId));
},
null, null, null, LocalizeText('catalog.marketplace.confirm_higher_title'));
break;
case 4:
simpleAlert(LocalizeText('catalog.alert.notenough.credits.description'), NotificationAlertType.DEFAULT, null, null, LocalizeText('catalog.alert.notenough.title'));
break;
}
});
return (
<>
<div className="relative inline-flex align-middle">
<Button active={ (searchType === MarketplaceSearchType.BY_ACTIVITY) } onClick={ () => setSearchType(MarketplaceSearchType.BY_ACTIVITY) }>
{ LocalizeText('catalog.marketplace.search_by_activity') }
</Button>
<Button active={ (searchType === MarketplaceSearchType.BY_VALUE) } onClick={ () => setSearchType(MarketplaceSearchType.BY_VALUE) }>
{ LocalizeText('catalog.marketplace.search_by_value') }
</Button>
<Button active={ (searchType === MarketplaceSearchType.ADVANCED) } onClick={ () => setSearchType(MarketplaceSearchType.ADVANCED) }>
{ LocalizeText('catalog.marketplace.search_advanced') }
</Button>
</div>
<SearchFormView searchType={ searchType } sortTypes={ getSortTypes } onSearch={ requestOffers } />
<Column gap={ 1 } overflow="hidden">
<Text shrink truncate fontWeight="bold">
{ LocalizeText('catalog.marketplace.items_found', [ 'count' ], [ offers.size.toString() ]) }
</Text>
<Column className="nitro-catalog-layout-marketplace-grid" overflow="auto">
{
Array.from(offers.values()).map((entry, index) => <CatalogLayoutMarketplaceItemView key={ index } offerData={ entry } type={ PUBLIC_OFFER } onClick={ purchaseItem } />)
}
</Column>
</Column>
</>
);
};
@@ -0,0 +1,86 @@
import { FC, useCallback, useEffect, useState } from 'react';
import { IMarketplaceSearchOptions, LocalizeText, MarketplaceSearchType } from '../../../../../../api';
import { Button, Text } from '../../../../../../common';
import { NitroInput } from '../../../../../../layout';
export interface SearchFormViewProps
{
searchType: number;
sortTypes: number[];
onSearch(options: IMarketplaceSearchOptions): void;
}
export const SearchFormView: FC<SearchFormViewProps> = props =>
{
const { searchType = null, sortTypes = null, onSearch = null } = props;
const [ sortType, setSortType ] = useState(sortTypes ? sortTypes[0] : 3); // first item of SORT_TYPES_ACTIVITY
const [ searchQuery, setSearchQuery ] = useState('');
const [ min, setMin ] = useState(0);
const [ max, setMax ] = useState(0);
const onSortTypeChange = useCallback((sortType: number) =>
{
setSortType(sortType);
if((searchType === MarketplaceSearchType.BY_ACTIVITY) || (searchType === MarketplaceSearchType.BY_VALUE)) onSearch({ minPrice: -1, maxPrice: -1, query: '', type: sortType });
}, [ onSearch, searchType ]);
const onClickSearch = useCallback(() =>
{
const minPrice = ((min > 0) ? min : -1);
const maxPrice = ((max > 0) ? max : -1);
onSearch({ minPrice: minPrice, maxPrice: maxPrice, type: sortType, query: searchQuery });
}, [ max, min, onSearch, searchQuery, sortType ]);
useEffect(() =>
{
if(!sortTypes || !sortTypes.length) return;
const sortType = sortTypes[0];
setSortType(sortType);
if(searchType === MarketplaceSearchType.BY_ACTIVITY || MarketplaceSearchType.BY_VALUE === searchType) onSearch({ minPrice: -1, maxPrice: -1, query: '', type: sortType });
}, [ onSearch, searchType, sortTypes ]);
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<Text className="col-span-3">{ LocalizeText('catalog.marketplace.sort_order') }</Text>
<select className="form-select form-select-sm" value={ sortType } onChange={ event => onSortTypeChange(parseInt(event.target.value)) }>
{ sortTypes.map(type => <option key={ type } value={ type }>{ LocalizeText(`catalog.marketplace.sort.${ type }`) }</option>) }
</select>
</div>
{ searchType === MarketplaceSearchType.ADVANCED &&
<>
<div className="flex items-center gap-1">
<Text className="col-span-3">{ LocalizeText('catalog.marketplace.search_name') }</Text>
<NitroInput
value={ searchQuery }
onChange={ event => setSearchQuery(event.target.value) } />
</div>
<div className="flex items-center gap-1">
<Text className="col-span-3">{ LocalizeText('catalog.marketplace.search_price') }</Text>
<div className="flex w-full gap-1">
<NitroInput
min={ 0 }
type="number"
value={ min }
onChange={ event => setMin(event.target.valueAsNumber) } />
<NitroInput
min={ 0 }
type="number"
value={ max }
onChange={ event => setMax(event.target.valueAsNumber) } />
</div>
</div>
<Button className="mx-auto" variant="secondary" onClick={ onClickSearch }>{ LocalizeText('generic.search') }</Button>
</> }
</div>
);
};
@@ -0,0 +1,124 @@
import { GetMarketplaceConfigurationMessageComposer, MakeOfferMessageComposer, MarketplaceConfigurationEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FurnitureItem, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../../../api';
import { Button, Column, Grid, LayoutFurniImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../../common';
import { CatalogPostMarketplaceOfferEvent } from '../../../../../../events';
import { useCatalog, useMessageEvent, useNotification, useUiEvent } from '../../../../../../hooks';
import { NitroInput } from '../../../../../../layout';
export const MarketplacePostOfferView: FC<{}> = props =>
{
const [ item, setItem ] = useState<FurnitureItem>(null);
const [ askingPrice, setAskingPrice ] = useState(0);
const [ tempAskingPrice, setTempAskingPrice ] = useState('0');
const { catalogOptions = null, setCatalogOptions = null } = useCatalog();
const { marketplaceConfiguration = null } = catalogOptions;
const { showConfirm = null } = useNotification();
const updateAskingPrice = (price: string) =>
{
setTempAskingPrice(price);
const newValue = parseInt(price);
if(isNaN(newValue) || (newValue === askingPrice)) return;
setAskingPrice(parseInt(price));
};
useMessageEvent<MarketplaceConfigurationEvent>(MarketplaceConfigurationEvent, event =>
{
const parser = event.getParser();
setCatalogOptions(prevValue =>
{
const newValue = { ...prevValue };
newValue.marketplaceConfiguration = parser;
return newValue;
});
});
useUiEvent<CatalogPostMarketplaceOfferEvent>(CatalogPostMarketplaceOfferEvent.POST_MARKETPLACE, event => setItem(event.item));
useEffect(() =>
{
if(!item || marketplaceConfiguration) return;
SendMessageComposer(new GetMarketplaceConfigurationMessageComposer());
}, [ item, marketplaceConfiguration ]);
useEffect(() =>
{
if(!item) return;
return () => setAskingPrice(0);
}, [ item ]);
if(!marketplaceConfiguration || !item) return null;
const getFurniTitle = (item ? LocalizeText(item.isWallItem ? 'wallItem.name.' + item.type : 'roomItem.name.' + item.type) : '');
const getFurniDescription = (item ? LocalizeText(item.isWallItem ? 'wallItem.desc.' + item.type : 'roomItem.desc.' + item.type) : '');
const getCommission = () => Math.max(Math.ceil(((marketplaceConfiguration.commission * 0.01) * askingPrice)), 1);
const postItem = () =>
{
if(!item || (askingPrice < marketplaceConfiguration.minimumPrice)) return;
showConfirm(LocalizeText('inventory.marketplace.confirm_offer.info', [ 'furniname', 'price' ], [ getFurniTitle, askingPrice.toString() ]), () =>
{
SendMessageComposer(new MakeOfferMessageComposer(askingPrice, item.isWallItem ? 2 : 1, item.id));
setItem(null);
},
() =>
{
setItem(null);
}, null, null, LocalizeText('inventory.marketplace.confirm_offer.title'));
};
return (
<NitroCardView className="nitro-catalog-layout-marketplace-post-offer" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('inventory.marketplace.make_offer.title') } onCloseClick={ event => setItem(null) } />
<NitroCardContentView overflow="hidden">
<Grid fullHeight>
<Column center className="bg-muted rounded p-2" overflow="hidden" size={ 4 }>
<LayoutFurniImageView extraData={ item.extra.toString() } productClassId={ item.type } productType={ item.isWallItem ? ProductTypeEnum.WALL : ProductTypeEnum.FLOOR } />
</Column>
<Column justifyContent="between" overflow="hidden" size={ 8 }>
<Column grow gap={ 1 }>
<Text fontWeight="bold">{ getFurniTitle }</Text>
<Text shrink truncate>{ getFurniDescription }</Text>
</Column>
<Column overflow="auto">
<Text italics>
{ LocalizeText('inventory.marketplace.make_offer.expiration_info', [ 'time' ], [ marketplaceConfiguration.offerTime.toString() ]) }
</Text>
<div className="input-group has-validation">
<NitroInput min={ 0 } placeholder={ LocalizeText('inventory.marketplace.make_offer.price_request') } type="number" value={ tempAskingPrice } onChange={ event => updateAskingPrice(event.target.value) } />
{ ((askingPrice < marketplaceConfiguration.minimumPrice) || isNaN(askingPrice)) &&
<div className="invalid-feedback d-block">
{ LocalizeText('inventory.marketplace.make_offer.min_price', [ 'minprice' ], [ marketplaceConfiguration.minimumPrice.toString() ]) }
</div> }
{ ((askingPrice > marketplaceConfiguration.maximumPrice) && !isNaN(askingPrice)) &&
<div className="invalid-feedback d-block">
{ LocalizeText('inventory.marketplace.make_offer.max_price', [ 'maxprice' ], [ marketplaceConfiguration.maximumPrice.toString() ]) }
</div> }
{ (!((askingPrice < marketplaceConfiguration.minimumPrice) || (askingPrice > marketplaceConfiguration.maximumPrice) || isNaN(askingPrice))) &&
<div className="invalid-feedback d-block">
{ LocalizeText('inventory.marketplace.make_offer.final_price', [ 'commission', 'finalprice' ], [ getCommission().toString(), (askingPrice + getCommission()).toString() ]) }
</div> }
</div>
<Button disabled={ ((askingPrice < marketplaceConfiguration.minimumPrice) || (askingPrice > marketplaceConfiguration.maximumPrice) || isNaN(askingPrice)) } onClick={ postItem }>
{ LocalizeText('inventory.marketplace.make_offer.post') }
</Button>
</Column>
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,243 @@
import { ApproveNameMessageComposer, ApproveNameMessageEvent, ColorConverter, GetSellablePetPalettesComposer, PurchaseFromCatalogComposer, SellablePetPaletteData } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaFillDrip } from 'react-icons/fa';
import { DispatchUiEvent, GetPetAvailableColors, GetPetIndexFromLocalization, LocalizeText, SendMessageComposer } from '../../../../../../api';
import { AutoGrid, Button, Column, Grid, LayoutGridItem, LayoutPetImageView, Text } from '../../../../../../common';
import { CatalogPurchaseFailureEvent } from '../../../../../../events';
import { useCatalog, useMessageEvent } from '../../../../../../hooks';
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';
export const CatalogLayoutPetView: FC<CatalogLayoutProps> = 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 [ petName, setPetName ] = useState('');
const [ approvalPending, setApprovalPending ] = useState(true);
const [ approvalResult, setApprovalResult ] = useState(-1);
const { currentOffer = null, setCurrentOffer = null, setPurchaseOptions = null, catalogOptions = null, roomPreviewer = null } = useCatalog();
const { petPalettes = null } = catalogOptions;
const getColor = useMemo(() =>
{
if(!sellableColors.length || (selectedColorIndex === -1)) return 0xFFFFFF;
return sellableColors[selectedColorIndex][0];
}, [ sellableColors, selectedColorIndex ]);
const petBreedName = useMemo(() =>
{
if((petIndex === -1) || !sellablePalettes.length || (selectedPaletteIndex === -1)) return '';
return LocalizeText(`pet.breed.${ petIndex }.${ sellablePalettes[selectedPaletteIndex].breedId }`);
}, [ petIndex, sellablePalettes, selectedPaletteIndex ]);
const petPurchaseString = useMemo(() =>
{
if(!sellablePalettes.length || (selectedPaletteIndex === -1)) return '';
const paletteId = sellablePalettes[selectedPaletteIndex].paletteId;
let color = 0xFFFFFF;
if(petIndex <= 7)
{
if(selectedColorIndex === -1) return '';
color = sellableColors[selectedColorIndex][0];
}
let colorString = color.toString(16).toUpperCase();
while(colorString.length < 6) colorString = ('0' + colorString);
return `${ paletteId }\n${ colorString }`;
}, [ sellablePalettes, selectedPaletteIndex, petIndex, sellableColors, selectedColorIndex ]);
const validationErrorMessage = useMemo(() =>
{
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;
}
if(!key || !key.length) return '';
return LocalizeText(key);
}, [ approvalResult ]);
const purchasePet = useCallback(() =>
{
if(approvalResult === -1)
{
SendMessageComposer(new ApproveNameMessageComposer(petName, 1));
return;
}
if(approvalResult === 0)
{
SendMessageComposer(new PurchaseFromCatalogComposer(page.pageId, currentOffer.offerId, `${ petName }\n${ petPurchaseString }`, 1));
return;
}
}, [ page, currentOffer, petName, petPurchaseString, approvalResult ]);
useMessageEvent<ApproveNameMessageEvent>(ApproveNameMessageEvent, event =>
{
const parser = event.getParser();
setApprovalResult(parser.result);
if(parser.result === 0) purchasePet();
else DispatchUiEvent(new CatalogPurchaseFailureEvent(-1));
});
useEffect(() =>
{
if(!page || !page.offers.length) return;
const offer = page.offers[0];
setCurrentOffer(offer);
setPetIndex(GetPetIndexFromLocalization(offer.localizationId));
setColorsShowing(false);
}, [ page, setCurrentOffer ]);
useEffect(() =>
{
if(!currentOffer) return;
const productData = currentOffer.product.productData;
if(!productData) return;
if(petPalettes)
{
for(const paletteData of petPalettes)
{
if(paletteData.breed !== productData.type) continue;
const palettes: SellablePetPaletteData[] = [];
for(const palette of paletteData.palettes)
{
if(!palette.sellable) continue;
palettes.push(palette);
}
setSelectedPaletteIndex((palettes.length ? 0 : -1));
setSellablePalettes(palettes);
return;
}
}
setSelectedPaletteIndex(-1);
setSellablePalettes([]);
SendMessageComposer(new GetSellablePetPalettesComposer(productData.type));
}, [ currentOffer, petPalettes ]);
useEffect(() =>
{
if(petIndex === -1) return;
const colors = GetPetAvailableColors(petIndex, sellablePalettes);
setSelectedColorIndex((colors.length ? 0 : -1));
setSellableColors(colors);
}, [ petIndex, sellablePalettes ]);
useEffect(() =>
{
if(!roomPreviewer) return;
roomPreviewer.reset(false);
if((petIndex === -1) || !sellablePalettes.length || (selectedPaletteIndex === -1)) return;
let petFigureString = `${ petIndex } ${ sellablePalettes[selectedPaletteIndex].paletteId }`;
if(petIndex <= 7) petFigureString += ` ${ getColor.toString(16) }`;
roomPreviewer.addPetIntoRoom(petFigureString);
}, [ roomPreviewer, petIndex, sellablePalettes, selectedPaletteIndex, getColor ]);
useEffect(() =>
{
setApprovalResult(-1);
}, [ petName ]);
if(!currentOffer) return null;
return (
<Grid>
<Column overflow="hidden" size={ 7 }>
<AutoGrid columnCount={ 5 }>
{ !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) =>
{
return (
<LayoutGridItem key={ index } itemActive={ (selectedPaletteIndex === index) } onClick={ event => setSelectedPaletteIndex(index) }>
<LayoutPetImageView direction={ 2 } headOnly={ true } paletteId={ palette.paletteId } typeId={ petIndex } />
</LayoutGridItem>
);
}) }
{ colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (selectedColorIndex === index) } itemColor={ ColorConverter.int2rgb(colorSet[0]) } onClick={ event => setSelectedColorIndex(index) } />) }
</AutoGrid>
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<div className="relative overflow-hidden">
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" position="absolute" />
{ ((petIndex > -1) && (petIndex <= 7)) &&
<Button className="bottom-1 start-1" position="absolute" onClick={ event => setColorsShowing(!colorsShowing) }>
<FaFillDrip className="fa-icon" />
</Button> }
</div>
<Column grow gap={ 1 }>
<Text truncate>{ petBreedName }</Text>
<Column grow gap={ 1 }>
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm w-full" placeholder={ LocalizeText('widgets.petpackage.name.title') } type="text" value={ petName } onChange={ event => setPetName(event.target.value) } />
{ (approvalResult > 0) &&
<div className="invalid-feedback d-block m-0">{ validationErrorMessage }</div> }
</Column>
<div className="flex justify-end">
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
</div>
<CatalogPurchaseWidgetView purchaseCallback={ purchasePet } />
</Column>
</> }
</Column>
</Grid>
);
};
@@ -0,0 +1,62 @@
import { SelectClubGiftComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo } from 'react';
import { LocalizeText, SendMessageComposer } from '../../../../../../api';
import { AutoGrid, Text } from '../../../../../../common';
import { useCatalog, useNotification, usePurse } from '../../../../../../hooks';
import { CatalogLayoutProps } from '../CatalogLayout.types';
import { VipGiftItem } from './VipGiftItemView';
export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
{
const { purse = null } = usePurse();
const { catalogOptions = null, setCatalogOptions = null } = useCatalog();
const { clubGifts = null } = catalogOptions;
const { showConfirm = null } = useNotification();
const giftsAvailable = useCallback(() =>
{
if(!clubGifts) return '';
if(clubGifts.giftsAvailable > 0) return LocalizeText('catalog.club_gift.available', [ 'amount' ], [ clubGifts.giftsAvailable.toString() ]);
if(clubGifts.daysUntilNextGift > 0) return LocalizeText('catalog.club_gift.days_until_next', [ 'days' ], [ clubGifts.daysUntilNextGift.toString() ]);
if(purse.isVip) return LocalizeText('catalog.club_gift.not_available');
return LocalizeText('catalog.club_gift.no_club');
}, [ clubGifts, purse ]);
const selectGift = useCallback((localizationId: string) =>
{
showConfirm(LocalizeText('catalog.club_gift.confirm'), () =>
{
SendMessageComposer(new SelectClubGiftComposer(localizationId));
setCatalogOptions(prevValue =>
{
prevValue.clubGifts.giftsAvailable--;
return { ...prevValue };
});
}, null);
}, [ setCatalogOptions, showConfirm ]);
const sortGifts = useMemo(() =>
{
let gifts = clubGifts.offers.sort((a,b) =>
{
return clubGifts.getOfferExtraData(a.offerId).daysRequired - clubGifts.getOfferExtraData(b.offerId).daysRequired;
});
return gifts;
},[ clubGifts ]);
return (
<>
<Text shrink truncate fontWeight="bold">{ giftsAvailable() }</Text>
<AutoGrid className="nitro-catalog-layout-vip-gifts-grid" columnCount={ 1 }>
{ (clubGifts.offers.length > 0) && sortGifts.map(offer => <VipGiftItem key={ offer.offerId } daysRequired={ clubGifts.getOfferExtraData(offer.offerId).daysRequired } isAvailable={ (clubGifts.getOfferExtraData(offer.offerId).isSelectable && (clubGifts.giftsAvailable > 0)) } offer={ offer } onSelect={ selectGift }/>) }
</AutoGrid>
</>
);
};
@@ -0,0 +1,63 @@
import { CatalogPageMessageOfferData } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { LocalizeText, ProductImageUtility } from '../../../../../../api';
import { Button, LayoutGridItem, LayoutImage, Text } from '../../../../../../common';
export interface VipGiftItemViewProps
{
offer: CatalogPageMessageOfferData;
isAvailable: boolean;
daysRequired: number;
onSelect(localizationId: string): void;
}
export const VipGiftItem : FC<VipGiftItemViewProps> = props =>
{
const { offer = null, isAvailable = false, daysRequired = 0, onSelect = null } = props;
const getImageUrlForOffer = useCallback( () =>
{
if(!offer || !offer.products.length) return '';
const productData = offer.products[0];
return ProductImageUtility.getProductImageUrl(productData.productType, productData.furniClassId, productData.extraParam);
}, [ offer ]);
const getItemTitle = useCallback(() =>
{
if(!offer || !offer.products.length) return '';
const productData = offer.products[0];
const localizationKey = ProductImageUtility.getProductCategory(productData.productType, productData.furniClassId) === 2 ? 'wallItem.name.' + productData.furniClassId : 'roomItem.name.' + productData.furniClassId;
return LocalizeText(localizationKey);
}, [ offer ]);
const getItemDesc = useCallback( () =>
{
if(!offer || !offer.products.length) return '';
const productData = offer.products[0];
const localizationKey = ProductImageUtility.getProductCategory(productData.productType, productData.furniClassId) === 2 ? 'wallItem.desc.' + productData.furniClassId : 'roomItem.desc.' + productData.furniClassId ;
return LocalizeText(localizationKey);
}, [ offer ]);
const getMonthsRequired = useCallback(() =>
{
return Math.floor(daysRequired / 31);
},[ daysRequired ]);
return (
<LayoutGridItem alignItems="center" center={ false } className="p-1" column={ false }>
<LayoutImage imageUrl={ getImageUrlForOffer() } />
<Text grow fontWeight="bold">{ getItemTitle() }</Text>
<Button disabled={ !isAvailable } variant="secondary" onClick={ () => onSelect(offer.localizationId) }>
{ LocalizeText('catalog.club_gift.select') }
</Button>
</LayoutGridItem>
);
};
@@ -0,0 +1,18 @@
import { FC } from 'react';
import { BaseProps, LayoutBadgeImageView } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
interface CatalogAddOnBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
{
}
export const CatalogAddOnBadgeWidgetView: FC<CatalogAddOnBadgeWidgetViewProps> = props =>
{
const { ...rest } = props;
const { currentOffer = null } = useCatalog();
if(!currentOffer || !currentOffer.badgeCode || !currentOffer.badgeCode.length) return null;
return <LayoutBadgeImageView badgeCode={ currentOffer.badgeCode } { ...rest } />;
};
@@ -0,0 +1,76 @@
import { StringDataType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { AutoGrid, AutoGridProps, LayoutBadgeImageView, LayoutGridItem } from '../../../../../common';
import { useCatalog, useInventoryBadges } from '../../../../../hooks';
const EXCLUDED_BADGE_CODES: string[] = [];
interface CatalogBadgeSelectorWidgetViewProps extends AutoGridProps
{
}
export const CatalogBadgeSelectorWidgetView: FC<CatalogBadgeSelectorWidgetViewProps> = props =>
{
const { columnCount = 5, ...rest } = props;
const [ isVisible, setIsVisible ] = useState(false);
const [ currentBadgeCode, setCurrentBadgeCode ] = useState<string>(null);
const { currentOffer = null, setPurchaseOptions = null } = useCatalog();
const { badgeCodes = [], activate = null, deactivate = null } = useInventoryBadges();
const previewStuffData = useMemo(() =>
{
if(!currentBadgeCode) return null;
const stuffData = new StringDataType();
stuffData.setValue([ '0', currentBadgeCode, '', '' ]);
return stuffData;
}, [ currentBadgeCode ]);
useEffect(() =>
{
if(!currentOffer) return;
setPurchaseOptions(prevValue =>
{
const newValue = { ...prevValue };
newValue.extraParamRequired = true;
newValue.extraData = ((previewStuffData && previewStuffData.getValue(1)) || null);
newValue.previewStuffData = previewStuffData;
return newValue;
});
}, [ currentOffer, previewStuffData, setPurchaseOptions ]);
useEffect(() =>
{
if(!isVisible) return;
const id = activate();
return () => deactivate(id);
}, [ isVisible, activate, deactivate ]);
useEffect(() =>
{
setIsVisible(true);
return () => setIsVisible(false);
}, []);
return (
<AutoGrid columnCount={ columnCount } { ...rest }>
{ badgeCodes && (badgeCodes.length > 0) && badgeCodes.map((badgeCode, index) =>
{
return (
<LayoutGridItem key={ index } itemActive={ (currentBadgeCode === badgeCode) } onClick={ event => setCurrentBadgeCode(badgeCode) }>
<LayoutBadgeImageView badgeCode={ badgeCode } />
</LayoutGridItem>
);
}) }
</AutoGrid>
);
};
@@ -0,0 +1,29 @@
import { FC, useEffect, useRef } from 'react';
import { AutoGrid, AutoGridProps, LayoutGridItem } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
interface CatalogBundleGridWidgetViewProps extends AutoGridProps
{
}
export const CatalogBundleGridWidgetView: FC<CatalogBundleGridWidgetViewProps> = props =>
{
const { columnCount = 5, children = null, ...rest } = props;
const { currentOffer = null } = useCatalog();
const elementRef = useRef<HTMLDivElement>();
useEffect(() =>
{
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ currentOffer ]);
if(!currentOffer) return null;
return (
<AutoGrid columnCount={ 5 } innerRef={ elementRef } { ...rest }>
{ currentOffer.products && (currentOffer.products.length > 0) && currentOffer.products.map((product, index) => <LayoutGridItem key={ index } itemCount={ product.productCount } itemImage={ product.getIconUrl() } />) }
{ children }
</AutoGrid>
);
};
@@ -0,0 +1,16 @@
import { FC, useEffect } from 'react';
import { useCatalog } from '../../../../../hooks';
export const CatalogFirstProductSelectorWidgetView: FC<{}> = props =>
{
const { currentPage = null, setCurrentOffer = null } = useCatalog();
useEffect(() =>
{
if(!currentPage || !currentPage.offers.length) return;
setCurrentOffer(currentPage.offers[0]);
}, [ currentPage, setCurrentOffer ]);
return null;
};
@@ -0,0 +1,31 @@
import { StringDataType } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react';
import { BaseProps, LayoutBadgeImageView } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
interface CatalogGuildBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
{
}
export const CatalogGuildBadgeWidgetView: FC<CatalogGuildBadgeWidgetViewProps> = props =>
{
const { ...rest } = props;
const { currentOffer = null, purchaseOptions = null } = useCatalog();
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 <LayoutBadgeImageView badgeCode={ badgeCode } isGroup={ true } { ...rest } />;
};
@@ -0,0 +1,75 @@
import { CatalogGroupsComposer, StringDataType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { LocalizeText, SendMessageComposer } from '../../../../../api';
import { Button, Flex } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
export const CatalogGuildSelectorWidgetView: FC<{}> = props =>
{
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
const { currentOffer = null, catalogOptions = null, setPurchaseOptions = null } = useCatalog();
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 newValue = { ...prevValue };
newValue.extraParamRequired = true;
newValue.extraData = ((previewStuffData && previewStuffData.getValue(1)) || null);
newValue.previewStuffData = previewStuffData;
return newValue;
});
}, [ currentOffer, previewStuffData, setPurchaseOptions ]);
useEffect(() =>
{
SendMessageComposer(new CatalogGroupsComposer());
}, []);
if(!groups || !groups.length)
{
return (
<div 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>
</div>
);
}
const selectedGroup = groups[selectedGroupIndex];
return (
<div className="flex gap-1">
{ !!selectedGroup &&
<Flex className="rounded border" overflow="hidden">
<div className="h-full" style={ { width: '20px', backgroundColor: '#' + selectedGroup.colorA } } />
<div className="h-full" 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>
</div>
);
};
@@ -0,0 +1,52 @@
import { FC, useEffect, useRef } from 'react';
import { IPurchasableOffer, ProductTypeEnum } from '../../../../../api';
import { AutoGrid, AutoGridProps } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
interface CatalogItemGridWidgetViewProps extends AutoGridProps
{
}
export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = props =>
{
const { columnCount = 5, children = null, ...rest } = props;
const { currentOffer = null, setCurrentOffer = null, currentPage = null, setPurchaseOptions = null } = useCatalog();
const elementRef = useRef<HTMLDivElement>();
useEffect(() =>
{
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ currentPage ]);
if(!currentPage) return null;
const selectOffer = (offer: IPurchasableOffer) =>
{
offer.activate();
if(offer.isLazy) return;
setCurrentOffer(offer);
if(offer.product && (offer.product.productType === ProductTypeEnum.WALL))
{
setPurchaseOptions(prevValue =>
{
const newValue = { ...prevValue };
newValue.extraData = (offer.product.extraParam || null);
return newValue;
});
}
};
return (
<AutoGrid columnCount={ columnCount } innerRef={ elementRef } { ...rest }>
{ currentPage.offers && (currentPage.offers.length > 0) && currentPage.offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />) }
{ children }
</AutoGrid>
);
};
@@ -0,0 +1,17 @@
import { FC } from 'react';
import { Offer } from '../../../../../api';
import { LayoutLimitedEditionCompletePlateView } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
export const CatalogLimitedItemWidgetView: FC = props =>
{
const { currentOffer = null } = useCatalog();
if(!currentOffer || (currentOffer.pricingModel !== Offer.PRICING_MODEL_SINGLE) || !currentOffer.product.isUniqueLimitedItem) return null;
return (
<div className="w-full">
<LayoutLimitedEditionCompletePlateView className="mx-auto" uniqueLimitedItemsLeft={ currentOffer.product.uniqueLimitedItemsLeft } uniqueLimitedSeriesSize={ currentOffer.product.uniqueLimitedItemSeriesSize } />
</div>
);
};
@@ -0,0 +1,37 @@
import { FC } from 'react';
import { FaPlus } from 'react-icons/fa';
import { IPurchasableOffer } from '../../../../../api';
import { LayoutCurrencyIcon, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
interface CatalogPriceDisplayWidgetViewProps
{
offer: IPurchasableOffer;
separator?: boolean;
}
export const CatalogPriceDisplayWidgetView: FC<CatalogPriceDisplayWidgetViewProps> = props =>
{
const { offer = null, separator = false } = props;
const { purchaseOptions = null } = useCatalog();
const { quantity = 1 } = purchaseOptions;
if(!offer) return null;
return (
<>
{ (offer.priceInCredits > 0) &&
<div className="flex items-center gap-1">
<Text bold>{ (offer.priceInCredits * quantity) }</Text>
<LayoutCurrencyIcon type={ -1 } />
</div> }
{ separator && (offer.priceInCredits > 0) && (offer.priceInActivityPoints > 0) &&
<FaPlus className="fa-icon" color="black" size="xs" /> }
{ (offer.priceInActivityPoints > 0) &&
<div className="flex items-center gap-1">
<Text bold>{ (offer.priceInActivityPoints * quantity) }</Text>
<LayoutCurrencyIcon type={ offer.activityPointType } />
</div> }
</>
);
};
@@ -0,0 +1,164 @@
import { CreateLinkEvent, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { CatalogPurchaseState, DispatchUiEvent, GetClubMemberLevel, LocalStorageKeys, LocalizeText, Offer, SendMessageComposer } from '../../../../../api';
import { Button, LayoutLoadingSpinnerView } from '../../../../../common';
import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, CatalogPurchasedEvent } from '../../../../../events';
import { useCatalog, useLocalStorage, usePurse, useUiEvent } from '../../../../../hooks';
interface CatalogPurchaseWidgetViewProps
{
noGiftOption?: boolean;
purchaseCallback?: () => void;
}
export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = props =>
{
const { noGiftOption = false, purchaseCallback = null } = props;
const [ purchaseWillBeGift, setPurchaseWillBeGift ] = useState(false);
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useLocalStorage(LocalStorageKeys.CATALOG_SKIP_PURCHASE_CONFIRMATION, false);
const { currentOffer = null, currentPage = null, purchaseOptions = null, setPurchaseOptions = null } = useCatalog();
const { getCurrencyAmount = null } = usePurse();
const onCatalogEvent = useCallback((event: CatalogEvent) =>
{
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(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
useUiEvent(CatalogPurchaseNotAllowedEvent.NOT_ALLOWED, onCatalogEvent);
useUiEvent(CatalogPurchaseSoldOutEvent.SOLD_OUT, onCatalogEvent);
const isLimitedSoldOut = useMemo(() =>
{
if(!currentOffer) return false;
if(purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)) return false;
if(currentOffer.pricingModel === Offer.PRICING_MODEL_SINGLE)
{
const product = currentOffer.product;
if(product && product.isUniqueLimitedItem) return !product.uniqueLimitedItemsLeft;
}
return false;
}, [ currentOffer, purchaseOptions ]);
const purchase = (isGift: boolean = false) =>
{
if(!currentOffer) return;
if(GetClubMemberLevel() < currentOffer.clubLevel)
{
CreateLinkEvent('habboUI/open/hccenter');
return;
}
if(isGift)
{
DispatchUiEvent(new CatalogInitGiftEvent(currentOffer.page.pageId, currentOffer.offerId, purchaseOptions.extraData));
return;
}
setPurchaseState(CatalogPurchaseState.PURCHASE);
if(purchaseCallback)
{
purchaseCallback();
return;
}
let pageId = currentOffer.page.pageId;
// if(pageId === -1)
// {
// const nodes = getNodesByOfferId(currentOffer.offerId);
// if(nodes) pageId = nodes[0].pageId;
// }
SendMessageComposer(new PurchaseFromCatalogComposer(pageId, currentOffer.offerId, purchaseOptions.extraData, purchaseOptions.quantity));
};
useEffect(() =>
{
if(!currentOffer) return;
setPurchaseState(CatalogPurchaseState.NONE);
}, [ currentOffer, setPurchaseOptions ]);
useEffect(() =>
{
let timeout: ReturnType<typeof setTimeout> = null;
if((purchaseState === CatalogPurchaseState.CONFIRM) || (purchaseState === CatalogPurchaseState.FAILED))
{
timeout = setTimeout(() => setPurchaseState(CatalogPurchaseState.NONE), 3000);
}
return () =>
{
if(timeout) clearTimeout(timeout);
};
}, [ purchaseState ]);
if(!currentOffer) return null;
const PurchaseButton = () =>
{
const priceCredits = (currentOffer.priceInCredits * purchaseOptions.quantity);
const pricePoints = (currentOffer.priceInActivityPoints * purchaseOptions.quantity);
if(GetClubMemberLevel() < currentOffer.clubLevel) return <Button disabled variant="danger">{ LocalizeText('catalog.alert.hc.required') }</Button>;
if(isLimitedSoldOut) return <Button disabled variant="danger">{ LocalizeText('catalog.alert.limited_edition_sold_out.title') }</Button>;
if(priceCredits > getCurrencyAmount(-1)) return <Button disabled variant="danger">{ LocalizeText('catalog.alert.notenough.title') }</Button>;
if(pricePoints > getCurrencyAmount(currentOffer.activityPointType)) return <Button disabled variant="danger">{ LocalizeText('catalog.alert.notenough.activitypoints.title.' + currentOffer.activityPointType) }</Button>;
switch(purchaseState)
{
case CatalogPurchaseState.CONFIRM:
return <Button variant="warning" onClick={ event => purchase() }>{ LocalizeText('catalog.marketplace.confirm_title') }</Button>;
case CatalogPurchaseState.PURCHASE:
return <Button disabled><LayoutLoadingSpinnerView /></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={ (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)) } onClick={ event => setPurchaseState(CatalogPurchaseState.CONFIRM) }>{ LocalizeText('catalog.purchase_confirmation.' + (currentOffer.isRentOffer ? 'rent' : 'buy')) }</Button>;
}
};
return (
<>
<PurchaseButton />
{ (!noGiftOption && !currentOffer.isRentOffer) &&
<Button disabled={ ((purchaseOptions.quantity > 1) || !currentOffer.giftable || isLimitedSoldOut || (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length))) } onClick={ event => purchase(true) }>
{ LocalizeText('catalog.purchase_confirmation.gift') }
</Button> }
</>
);
};
@@ -0,0 +1,14 @@
import { FC } from 'react';
import { useCatalog } from '../../../../../hooks';
import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
export const CatalogSimplePriceWidgetView: FC<{}> = props =>
{
const { currentOffer = null } = useCatalog();
return (
<div className="flex items-center bg-muted p-1 rounded gap-1">
<CatalogPriceDisplayWidgetView offer={ currentOffer } separator={ true } />
</div>
);
};
@@ -0,0 +1,7 @@
import { FC } from 'react';
import { CatalogFirstProductSelectorWidgetView } from './CatalogFirstProductSelectorWidgetView';
export const CatalogSingleViewWidgetView: FC<{}> = props =>
{
return <CatalogFirstProductSelectorWidgetView />;
};
@@ -0,0 +1,115 @@
import { FC, useEffect, useRef, useState } from 'react';
import { IPurchasableOffer, LocalizeText, Offer, ProductTypeEnum } from '../../../../../api';
import { AutoGrid, AutoGridProps, Button } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
interface CatalogSpacesWidgetViewProps extends AutoGridProps
{
}
const SPACES_GROUP_NAMES = [ 'floors', 'walls', 'views' ];
export const CatalogSpacesWidgetView: FC<CatalogSpacesWidgetViewProps> = props =>
{
const { columnCount = 5, 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 } = useCatalog();
const elementRef = useRef<HTMLDivElement>();
const setSelectedOffer = (offer: IPurchasableOffer) =>
{
if(!offer) return;
setSelectedOfferForGroup(prevValue =>
{
const newValue = [ ...prevValue ];
newValue[selectedGroupIndex] = offer;
return newValue;
});
};
useEffect(() =>
{
if(!currentPage) return;
const groupedOffers: IPurchasableOffer[][] = [ [], [], [] ];
for(const offer of currentPage.offers)
{
if((offer.pricingModel !== Offer.PRICING_MODEL_SINGLE) && (offer.pricingModel !== Offer.PRICING_MODEL_MULTI)) continue;
const product = offer.product;
if(!product || ((product.productType !== ProductTypeEnum.WALL) && (product.productType !== ProductTypeEnum.FLOOR)) || !product.furnitureData) continue;
const className = product.furnitureData.className;
switch(className)
{
case 'floor':
groupedOffers[0].push(offer);
break;
case 'wallpaper':
groupedOffers[1].push(offer);
break;
case 'landscape':
groupedOffers[2].push(offer);
break;
}
}
setGroupedOffers(groupedOffers);
setSelectedGroupIndex(0);
setSelectedOfferForGroup([ groupedOffers[0][0], groupedOffers[1][0], groupedOffers[2][0] ]);
}, [ currentPage ]);
useEffect(() =>
{
if((selectedGroupIndex === -1) || !selectedOfferForGroup) return;
setCurrentOffer(selectedOfferForGroup[selectedGroupIndex]);
}, [ selectedGroupIndex, selectedOfferForGroup, setCurrentOffer ]);
useEffect(() =>
{
if((selectedGroupIndex === -1) || !selectedOfferForGroup || !currentOffer) return;
setPurchaseOptions(prevValue =>
{
const newValue = { ...prevValue };
newValue.extraData = selectedOfferForGroup[selectedGroupIndex].product.extraParam;
newValue.extraParamRequired = true;
return newValue;
});
}, [ currentOffer, selectedGroupIndex, selectedOfferForGroup, setPurchaseOptions ]);
useEffect(() =>
{
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ selectedGroupIndex ]);
if(!groupedOffers || (selectedGroupIndex === -1)) return null;
const offers = groupedOffers[selectedGroupIndex];
return (
<>
<div className="relative inline-flex align-middle">
{ SPACES_GROUP_NAMES.map((name, index) => <Button key={ index } active={ (selectedGroupIndex === index) } onClick={ event => setSelectedGroupIndex(index) }>{ LocalizeText(`catalog.spaces.tab.${ name }`) }</Button>) }
</div>
<AutoGrid columnCount={ columnCount } innerRef={ elementRef } { ...rest }>
{ offers && (offers.length > 0) && offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer === offer)) } offer={ offer } selectOffer={ offer => setSelectedOffer(offer) } />) }
{ children }
</AutoGrid>
</>
);
};
@@ -0,0 +1,46 @@
import { FC } from 'react';
import { FaCaretLeft, FaCaretRight } from 'react-icons/fa';
import { LocalizeText } from '../../../../../api';
import { Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
const MIN_VALUE: number = 1;
const MAX_VALUE: number = 100;
export const CatalogSpinnerWidgetView: FC<{}> = props =>
{
const { currentOffer = null, purchaseOptions = null, setPurchaseOptions = null } = useCatalog();
const { quantity = 1 } = purchaseOptions;
const updateQuantity = (value: number) =>
{
if(isNaN(value)) value = 1;
value = Math.max(value, MIN_VALUE);
value = Math.min(value, MAX_VALUE);
if(value === quantity) return;
setPurchaseOptions(prevValue =>
{
const newValue = { ...prevValue };
newValue.quantity = value;
return newValue;
});
};
if(!currentOffer || !currentOffer.bundlePurchaseAllowed) return null;
return (
<>
<Text>{ LocalizeText('catalog.bundlewidget.spinner.select.amount') }</Text>
<div className="flex items-center gap-1">
<FaCaretLeft className="text-black cursor-pointer fa-icon" onClick={ event => updateQuantity(quantity - 1) } />
<input className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none min-h-[17px] h-[17px] w-[28px] px-[4px] py-[0] text-right rounded-[.2rem]" type="number" value={ quantity } onChange={ event => updateQuantity(event.target.valueAsNumber) } />
<FaCaretRight className="text-black cursor-pointer fa-icon" onClick={ event => updateQuantity(quantity + 1) } />
</div>
</>
);
};
@@ -0,0 +1,20 @@
import { FC } from 'react';
import { Column, ColumnProps } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
interface CatalogSimplePriceWidgetViewProps extends ColumnProps
{
}
export const CatalogTotalPriceWidget: FC<CatalogSimplePriceWidgetViewProps> = props =>
{
const { gap = 1, ...rest } = props;
const { currentOffer = null } = useCatalog();
return (
<Column gap={ gap } { ...rest }>
<CatalogPriceDisplayWidgetView offer={ currentOffer } />
</Column>
);
};
@@ -0,0 +1,99 @@
import { GetAvatarRenderManager, GetSessionDataManager, Vector3d } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react';
import { FurniCategory, Offer, ProductTypeEnum } from '../../../../../api';
import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
export const CatalogViewProductWidgetView: FC<{}> = props =>
{
const { currentOffer = null, roomPreviewer = null, purchaseOptions = null } = useCatalog();
const { previewStuffData = null } = purchaseOptions;
useEffect(() =>
{
if(!currentOffer || (currentOffer.pricingModel === Offer.PRICING_MODEL_BUNDLE) || !roomPreviewer) return;
const product = currentOffer.product;
if(!product) return;
roomPreviewer.reset(false);
switch(product.productType)
{
case ProductTypeEnum.FLOOR: {
if(!product.furnitureData) return;
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[] = [];
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.updateObjectRoom(product.extraParam);
return;
case FurniCategory.WALL_PAPER:
roomPreviewer.updateObjectRoom(null, product.extraParam);
return;
case FurniCategory.LANDSCAPE: {
roomPreviewer.updateObjectRoom(null, null, product.extraParam);
const furniData = GetSessionDataManager().getWallItemDataByName('window_double_default');
if(furniData) roomPreviewer.addWallItemIntoRoom(furniData.id, new Vector3d(90), furniData.customParams);
return;
}
default:
roomPreviewer.updateObjectRoom('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)
{
return (
<Column fit className="bg-muted p-2 rounded" overflow="hidden">
<AutoGrid fullWidth className="nitro-catalog-layout-bundle-grid" columnCount={ 4 }>
{ (currentOffer.products.length > 0) && currentOffer.products.map((product, index) =>
{
return <LayoutGridItem key={ index } itemCount={ product.productCount } itemImage={ product.getIconUrl(currentOffer) } />;
}) }
</AutoGrid>
</Column>
);
}
return <LayoutRoomPreviewerView height={ 140 } roomPreviewer={ roomPreviewer } />;
};