mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
🆙 Init V3
This commit is contained in:
@@ -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 } />;
|
||||
}
|
||||
};
|
||||
+37
@@ -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>
|
||||
);
|
||||
};
|
||||
+83
@@ -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>
|
||||
);
|
||||
};
|
||||
+102
@@ -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>
|
||||
);
|
||||
};
|
||||
+161
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+86
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user