🆙 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,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 } />;
};