mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
feat: add builders club catalog ui flow
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../../api';
|
||||
import { CatalogType, LocalizeText } from '../../../../api';
|
||||
import { useCatalog } from '../../../../hooks';
|
||||
import { IPageEditData, useCatalogAdmin } from '../../CatalogAdminContext';
|
||||
|
||||
@@ -8,15 +8,20 @@ const LAYOUT_OPTIONS = [
|
||||
'default_3x3', 'frontpage4', 'pets', 'pets2', 'pets3',
|
||||
'spaces_new', 'soundmachine', 'trophies', 'roomads',
|
||||
'guild_frontpage', 'guild_forum', 'guild_custom_furni',
|
||||
'vip_buy', 'marketplace', 'marketplace_own_items',
|
||||
'vip_buy', 'builders_club_frontpage', 'builders_club_addons', 'builders_club_loyalty', 'marketplace', 'marketplace_own_items',
|
||||
'recycler', 'recycler_info', 'recycler_prizes',
|
||||
'info_loyalty', 'badge_display', 'bots', 'single_bundle',
|
||||
'color_grouping', 'recent_purchases', 'custom_prefix'
|
||||
];
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ value: CatalogType.NORMAL, label: 'Normale' },
|
||||
{ value: 'BOTH', label: 'Entrambi' }
|
||||
];
|
||||
|
||||
export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
{
|
||||
const { currentPage = null, activeNodes = [], rootNode = null } = useCatalog();
|
||||
const { currentPage = null, activeNodes = [], rootNode = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const editingPageData = catalogAdmin?.editingPageData ?? false;
|
||||
const editingRootPage = catalogAdmin?.editingRootPage ?? false;
|
||||
@@ -24,6 +29,7 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
const loading = catalogAdmin?.loading ?? false;
|
||||
|
||||
const [ caption, setCaption ] = useState('');
|
||||
const [ catalogMode, setCatalogMode ] = useState(CatalogType.NORMAL);
|
||||
const [ pageLayout, setPageLayout ] = useState('default_3x3');
|
||||
const [ minRank, setMinRank ] = useState(1);
|
||||
const [ visible, setVisible ] = useState('1');
|
||||
@@ -55,12 +61,13 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
if(!editingPageData || !targetNode) return;
|
||||
|
||||
setCaption(targetNode.localization || '');
|
||||
setCatalogMode(currentType === CatalogType.BUILDER ? CatalogType.BUILDER : (currentType || CatalogType.NORMAL));
|
||||
setPageLayout(currentPage?.layoutCode || 'default_3x3');
|
||||
setVisible(targetNode.isVisible ? '1' : '0');
|
||||
setEnabled('1');
|
||||
setMinRank(1);
|
||||
setOrderNum(0);
|
||||
}, [ editingPageData, targetNode, currentPage ]);
|
||||
}, [ editingPageData, targetNode, currentPage, currentType ]);
|
||||
|
||||
if(!editingPageData || !targetNode) return null;
|
||||
|
||||
@@ -75,6 +82,7 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
const data: IPageEditData = {
|
||||
pageId: targetPageId,
|
||||
caption,
|
||||
catalogMode,
|
||||
pageLayout,
|
||||
minRank,
|
||||
visible,
|
||||
@@ -116,6 +124,14 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Min Rank</label>
|
||||
<input className={ inputClass } min={ 1 } type="number" value={ minRank } onChange={ e => setMinRank(parseInt(e.target.value) || 1) } />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Mode</label>
|
||||
{ currentType === CatalogType.BUILDER
|
||||
? <div className={ `${ inputClass } flex items-center min-h-[28px] bg-gray-100 text-muted` }>Builders Club</div>
|
||||
: <select className={ inputClass } value={ catalogMode } onChange={ e => setCatalogMode(e.target.value) }>
|
||||
{ MODE_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ option.label }</option>) }
|
||||
</select> }
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Layout</label>
|
||||
<select className={ inputClass } value={ pageLayout } onChange={ e => setPageLayout(e.target.value) }>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { GetTickerTime } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { CatalogType, FriendlyTime, LocalizeText } from '../../../../api';
|
||||
import buildersClubIcon from '../../../../assets/images/toolbar/icons/buildersclub.png';
|
||||
import { useCatalog } from '../../../../hooks';
|
||||
|
||||
export const CatalogBuildersClubStatusView: FC = () =>
|
||||
{
|
||||
const { currentType = CatalogType.NORMAL, furniCount = 0, furniLimit = 0, secondsLeft = 0, secondsLeftWithGrace = 0, updateTime = 0 } = useCatalog();
|
||||
const [ ticker, setTicker ] = useState(() => GetTickerTime());
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(currentType !== CatalogType.BUILDER) return;
|
||||
|
||||
const interval = window.setInterval(() => setTicker(GetTickerTime()), 1000);
|
||||
|
||||
return () => window.clearInterval(interval);
|
||||
}, [ currentType ]);
|
||||
|
||||
const localizeOrDefault = (key: string, fallback: string, parameters: string[] = [], values: string[] = []) =>
|
||||
{
|
||||
const localized = LocalizeText(key, parameters, values);
|
||||
|
||||
return ((localized && (localized !== key)) ? localized : fallback);
|
||||
};
|
||||
|
||||
const remainingSeconds = useMemo(() =>
|
||||
{
|
||||
const baseSeconds = (secondsLeft > 0) ? secondsLeft : secondsLeftWithGrace;
|
||||
|
||||
if(baseSeconds <= 0) return 0;
|
||||
|
||||
const elapsed = ((updateTime > 0) ? Math.floor((ticker - updateTime) / 1000) : 0);
|
||||
|
||||
return Math.max(0, (baseSeconds - elapsed));
|
||||
}, [ secondsLeft, secondsLeftWithGrace, ticker, updateTime ]);
|
||||
|
||||
const isFullMember = (secondsLeft > 0);
|
||||
const membershipStatus = localizeOrDefault(
|
||||
isFullMember ? 'builder.header.status.member' : 'builder.header.status.trial',
|
||||
isFullMember ? 'Membro Completo' : 'Prova Gratuita'
|
||||
);
|
||||
|
||||
const title = localizeOrDefault(
|
||||
'builder.header.title',
|
||||
`Stato Builders' Club: ${ membershipStatus }`,
|
||||
[ 'BCSTATUS' ],
|
||||
[ membershipStatus ]
|
||||
);
|
||||
|
||||
const durationText = localizeOrDefault(
|
||||
'builder.header.status.membership',
|
||||
`Tempo mancante: ${ FriendlyTime.format(remainingSeconds) }`,
|
||||
[ 'DURATION' ],
|
||||
[ FriendlyTime.format(remainingSeconds) ]
|
||||
);
|
||||
|
||||
const limitText = localizeOrDefault(
|
||||
'builder.header.status.limit',
|
||||
`Furni usati: ${ furniCount }/${ furniLimit }`,
|
||||
[ 'COUNT', 'LIMIT' ],
|
||||
[ furniCount.toString(), furniLimit.toString() ]
|
||||
);
|
||||
|
||||
if(currentType !== CatalogType.BUILDER) return null;
|
||||
|
||||
return (
|
||||
<div className="builders-club-status-shell flex items-center gap-3 px-4 py-3">
|
||||
<div className="builders-club-status-icon-shell flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-md">
|
||||
<img alt="" className="h-[28px] w-[28px] object-contain" src={ buildersClubIcon } />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate text-[13px] leading-none font-bold text-white">
|
||||
{ title }
|
||||
</span>
|
||||
<span className="mt-1 text-[11px] leading-tight text-white/95">
|
||||
{ durationText }
|
||||
</span>
|
||||
<span className="text-[11px] leading-tight text-[#ffba45]">
|
||||
{ limitText }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, useCallback, useRef, useState } from 'react';
|
||||
import { FaArrowsAlt, FaCaretDown, FaCaretUp, FaPlus, FaStar, FaTrash } from 'react-icons/fa';
|
||||
import { ICatalogNode, LocalizeText } from '../../../../api';
|
||||
import { CatalogType, ICatalogNode, LocalizeText } from '../../../../api';
|
||||
import { useCatalog, useCatalogFavorites } from '../../../../hooks';
|
||||
import { useCatalogAdmin } from '../../CatalogAdminContext';
|
||||
import { CatalogIconView } from '../catalog-icon/CatalogIconView';
|
||||
@@ -15,7 +15,7 @@ export interface CatalogNavigationItemViewProps
|
||||
export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = props =>
|
||||
{
|
||||
const { node = null, child = false } = props;
|
||||
const { activateNode = null } = useCatalog();
|
||||
const { activateNode = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const { isFavoritePage, toggleFavoritePage } = useCatalogFavorites();
|
||||
@@ -100,6 +100,7 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
|
||||
e.stopPropagation();
|
||||
catalogAdmin.createPage({
|
||||
caption: 'New Page',
|
||||
catalogMode: currentType,
|
||||
pageLayout: 'default_3x3',
|
||||
minRank: 1,
|
||||
visible: '1',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MouseEventType } from '@nitrots/nitro-renderer';
|
||||
import { FC, MouseEvent, useMemo, useState } from 'react';
|
||||
import { FaHeart } from 'react-icons/fa';
|
||||
import { IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api';
|
||||
import { CatalogType, IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api';
|
||||
import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common';
|
||||
import { useCatalog, useCatalogFavorites, useInventoryFurni } from '../../../../../hooks';
|
||||
|
||||
@@ -15,7 +15,7 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
|
||||
{
|
||||
const { offer = null, selectOffer = null, itemActive = false, ...rest } = props;
|
||||
const [ isMouseDown, setMouseDown ] = useState(false);
|
||||
const { requestOfferToMover = null } = useCatalog();
|
||||
const { requestOfferToMover = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const { isVisible = false } = useInventoryFurni();
|
||||
const { isFavoriteOffer, toggleFavoriteOffer } = useCatalogFavorites();
|
||||
const isFav = offer ? isFavoriteOffer(offer.offerId) : false;
|
||||
@@ -44,7 +44,9 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
|
||||
setMouseDown(false);
|
||||
return;
|
||||
case MouseEventType.ROLL_OUT:
|
||||
if(!isMouseDown || !itemActive || !isVisible) return;
|
||||
if(!isMouseDown || !itemActive) return;
|
||||
if(currentType === CatalogType.BUILDER) return;
|
||||
if(!isVisible) return;
|
||||
|
||||
requestOfferToMover(offer);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CatalogPurchaseState, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common';
|
||||
import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events';
|
||||
import { useCatalog, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
const BUILDERS_CLUB_WINDOW_ID = 2;
|
||||
const BUILDERS_CLUB_ADDONS_WINDOW_ID = 3;
|
||||
|
||||
export const CatalogLayoutBuildersClubBuyView: FC<CatalogLayoutProps> = () =>
|
||||
{
|
||||
const [ pendingOffer, setPendingOffer ] = useState<ClubOfferData>(null);
|
||||
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
|
||||
const { currentPage = null, catalogOptions = null } = useCatalog();
|
||||
const { getCurrencyAmount = null } = usePurse();
|
||||
const isPurchasingRef = useRef(false);
|
||||
const isAddonLayout = (currentPage?.layoutCode === 'builders_club_addons');
|
||||
const windowId = (isAddonLayout ? BUILDERS_CLUB_ADDONS_WINDOW_ID : BUILDERS_CLUB_WINDOW_ID);
|
||||
const offers = catalogOptions?.clubOffersByWindowId?.[windowId] || null;
|
||||
|
||||
const onCatalogEvent = useCallback((event: CatalogEvent) =>
|
||||
{
|
||||
switch(event.type)
|
||||
{
|
||||
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
|
||||
isPurchasingRef.current = false;
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
return;
|
||||
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
|
||||
isPurchasingRef.current = false;
|
||||
setPurchaseState(CatalogPurchaseState.FAILED);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
|
||||
useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
|
||||
|
||||
const getOfferTotalUnits = useCallback((offer: ClubOfferData) =>
|
||||
{
|
||||
if(!offer) return 0;
|
||||
|
||||
return ((offer.months * 31) + offer.extraDays);
|
||||
}, []);
|
||||
|
||||
const getOfferName = useCallback((offer: ClubOfferData) =>
|
||||
{
|
||||
if(!offer) return '';
|
||||
|
||||
const localized = LocalizeText(offer.productCode);
|
||||
|
||||
if(localized && (localized !== offer.productCode)) return localized;
|
||||
|
||||
return offer.productCode.replace(/_/g, ' ');
|
||||
}, []);
|
||||
|
||||
const getOfferMeta = useCallback((offer: ClubOfferData) =>
|
||||
{
|
||||
if(!offer) return '';
|
||||
|
||||
if(isAddonLayout)
|
||||
{
|
||||
const units = getOfferTotalUnits(offer);
|
||||
|
||||
return (units > 0) ? `+${ units }` : '';
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if(offer.months > 0) parts.push(LocalizeText('catalog.vip.item.header.months', [ 'num_months' ], [ offer.months.toString() ]));
|
||||
if(offer.extraDays > 0) parts.push(LocalizeText('catalog.vip.item.header.days', [ 'num_days' ], [ offer.extraDays.toString() ]));
|
||||
|
||||
return parts.join(' ');
|
||||
}, [ getOfferTotalUnits, isAddonLayout ]);
|
||||
|
||||
const purchaseOffer = useCallback(() =>
|
||||
{
|
||||
if(!pendingOffer || !currentPage || isPurchasingRef.current) return;
|
||||
|
||||
isPurchasingRef.current = true;
|
||||
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
||||
SendMessageComposer(new PurchaseFromCatalogComposer(currentPage.pageId, pendingOffer.offerId, null, 1));
|
||||
}, [ pendingOffer, currentPage ]);
|
||||
|
||||
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={ purchaseOffer }>{ 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>;
|
||||
}
|
||||
}, [ getCurrencyAmount, pendingOffer, purchaseOffer, purchaseState ]);
|
||||
|
||||
const pageDescription = useMemo(() =>
|
||||
{
|
||||
if(!currentPage) return '';
|
||||
|
||||
return currentPage.localization.getText(1) || currentPage.localization.getText(2) || currentPage.localization.getText(0) || '';
|
||||
}, [ currentPage ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!offers) SendMessageComposer(new GetClubOffersMessageComposer(windowId));
|
||||
}, [ offers, windowId ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!offers || !offers.length) return;
|
||||
|
||||
setPendingOffer(prevValue =>
|
||||
{
|
||||
if(prevValue && offers.some(offer => (offer.offerId === prevValue.offerId))) return prevValue;
|
||||
|
||||
return offers[0];
|
||||
});
|
||||
}, [ offers ]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2">
|
||||
{ currentPage?.localization?.getImage(0) &&
|
||||
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
|
||||
<Grid>
|
||||
<Column fullHeight justifyContent="between" overflow="hidden" size={ 7 }>
|
||||
<Column gap={ 1 } overflow="auto">
|
||||
{ offers && (offers.length > 0) && offers.map((offer, index) =>
|
||||
{
|
||||
const meta = getOfferMeta(offer);
|
||||
|
||||
return (
|
||||
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-2" column={ false } itemActive={ pendingOffer?.offerId === offer.offerId } justifyContent="between" onClick={ () => {
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
setPendingOffer(offer);
|
||||
} }>
|
||||
<Column gap={ 0 }>
|
||||
<Text fontWeight="bold">{ getOfferName(offer) }</Text>
|
||||
{ meta.length > 0 && <Text small>{ meta }</Text> }
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
{ (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> }
|
||||
</div>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}) }
|
||||
</Column>
|
||||
</Column>
|
||||
<Column gap={ 2 } overflow="hidden" size={ 5 }>
|
||||
<Column center grow overflow="hidden">
|
||||
{ currentPage?.localization.getImage(1) && <img alt="" src={ currentPage.localization.getImage(1) } /> }
|
||||
{ pageDescription.length > 0 && <Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(pageDescription) } } overflow="auto" /> }
|
||||
</Column>
|
||||
{ pendingOffer &&
|
||||
<Column fullWidth gap={ 1 }>
|
||||
<Text fontWeight="bold">{ getOfferName(pendingOffer) }</Text>
|
||||
{ getOfferMeta(pendingOffer).length > 0 && <Text>{ getOfferMeta(pendingOffer) }</Text> }
|
||||
<Flex alignItems="end">
|
||||
<Column grow gap={ 0 }>
|
||||
<Text>{ currentPage?.localization.getText(0) || '' }</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,8 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
|
||||
const { currentPage = null, catalogOptions = null } = useCatalog();
|
||||
const { purse = null, getCurrencyAmount = null } = usePurse();
|
||||
const { clubOffers = null } = catalogOptions;
|
||||
const { clubOffers = null, clubOffersByWindowId = null } = (catalogOptions || {});
|
||||
const offers = clubOffersByWindowId?.[1] || clubOffers;
|
||||
const isPurchasingRef = useRef<boolean>(false);
|
||||
|
||||
const onCatalogEvent = useCallback((event: CatalogEvent) =>
|
||||
@@ -129,14 +130,14 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!clubOffers) SendMessageComposer(new GetClubOffersMessageComposer(1));
|
||||
}, [ clubOffers ]);
|
||||
if(!offers) SendMessageComposer(new GetClubOffersMessageComposer(1));
|
||||
}, [ offers ]);
|
||||
|
||||
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) =>
|
||||
{ offers && (offers.length > 0) && offers.map((offer, index) =>
|
||||
{
|
||||
return (
|
||||
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-1" column={ false } itemActive={ pendingOffer === offer } justifyContent="between" onClick={ () => setOffer(offer) }>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ICatalogPage } from '../../../../../api';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
import { CatalogLayoutBadgeDisplayView } from './CatalogLayoutBadgeDisplayView';
|
||||
import { CatalogLayoutBuildersClubBuyView } from './CatalogLayoutBuildersClubBuyView';
|
||||
import { CatalogLayoutColorGroupingView } from './CatalogLayoutColorGroupingView';
|
||||
import { CatalogLayoutCustomPrefixView } from './CatalogLayoutCustomPrefixView';
|
||||
import { CatalogLayoutDefaultView } from './CatalogLayoutDefaultView';
|
||||
@@ -43,6 +44,10 @@ export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void)
|
||||
return <CatalogLayoutPets3View { ...layoutProps } />;
|
||||
case 'vip_buy':
|
||||
return <CatalogLayoutVipBuyView { ...layoutProps } />;
|
||||
case 'builders_club_frontpage':
|
||||
case 'builders_club_addons':
|
||||
case 'builders_club_loyalty':
|
||||
return <CatalogLayoutBuildersClubBuyView { ...layoutProps } />;
|
||||
case 'guild_frontpage':
|
||||
return <CatalogLayouGuildFrontpageView { ...layoutProps } />;
|
||||
case 'guild_forum':
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CreateLinkEvent, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CatalogPurchaseState, DispatchUiEvent, GetClubMemberLevel, LocalStorageKeys, LocalizeText, Offer, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, LayoutLoadingSpinnerView } from '../../../../../common';
|
||||
import { BuilderFurniPlaceableStatus, CatalogPurchaseState, CatalogType, DispatchUiEvent, GetClubMemberLevel, LocalStorageKeys, LocalizeText, NotificationBubbleType, Offer, ProductTypeEnum, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, LayoutLoadingSpinnerView, Text } from '../../../../../common';
|
||||
import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, CatalogPurchasedEvent } from '../../../../../events';
|
||||
import { useCatalog, useLocalStorage, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
import { useCatalog, useLocalStorage, useNotification, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
|
||||
interface CatalogPurchaseWidgetViewProps
|
||||
{
|
||||
@@ -16,11 +16,13 @@ let isPurchasingCatalogItem = false;
|
||||
export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = props =>
|
||||
{
|
||||
const { noGiftOption = false, purchaseCallback = null } = props;
|
||||
const [ builderPlaceableRefreshTick, setBuilderPlaceableRefreshTick ] = useState(0);
|
||||
const [ purchaseWillBeGift, setPurchaseWillBeGift ] = useState(false);
|
||||
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
|
||||
const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useLocalStorage(LocalStorageKeys.CATALOG_SKIP_PURCHASE_CONFIRMATION, false);
|
||||
const { currentOffer = null, currentPage = null, purchaseOptions = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { currentOffer = null, currentPage = null, currentType = CatalogType.NORMAL, purchaseOptions = null, setPurchaseOptions = null, requestOfferToMover = null, setCatalogPlaceMultipleObjects = null, getBuilderFurniPlaceableStatus = null } = useCatalog();
|
||||
const { getCurrencyAmount = null } = usePurse();
|
||||
const { showSingleBubble = null } = useNotification();
|
||||
|
||||
const onCatalogEvent = useCallback((event: CatalogEvent) =>
|
||||
{
|
||||
@@ -132,8 +134,79 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
|
||||
if(!currentOffer) return null;
|
||||
|
||||
const isBuildersClubOffer = (currentType === CatalogType.BUILDER);
|
||||
const isBuildersClubPlaceable = isBuildersClubOffer
|
||||
&& !!currentOffer.product
|
||||
&& ((currentOffer.product.productType === ProductTypeEnum.FLOOR) || (currentOffer.product.productType === ProductTypeEnum.WALL));
|
||||
const builderPlaceableStatus = useMemo(() =>
|
||||
{
|
||||
if(!isBuildersClubPlaceable || !getBuilderFurniPlaceableStatus) return BuilderFurniPlaceableStatus.OKAY;
|
||||
|
||||
return getBuilderFurniPlaceableStatus(currentOffer);
|
||||
}, [ currentOffer, getBuilderFurniPlaceableStatus, isBuildersClubPlaceable, builderPlaceableRefreshTick ]);
|
||||
const buildersClubPlaceOneButtonStyle = useMemo(() => ({
|
||||
background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)',
|
||||
borderColor: '#d79d2e',
|
||||
color: '#ffffff'
|
||||
}), []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isBuildersClubPlaceable) return;
|
||||
|
||||
const interval = setInterval(() => setBuilderPlaceableRefreshTick(prevValue => (prevValue + 1)), 500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [ isBuildersClubPlaceable ]);
|
||||
|
||||
const PurchaseButton = () =>
|
||||
{
|
||||
if(isBuildersClubPlaceable)
|
||||
{
|
||||
const hasMissingExtraParam = (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length));
|
||||
const isBlockedByVisitors = (builderPlaceableStatus === BuilderFurniPlaceableStatus.VISITORS_IN_ROOM);
|
||||
const isDisabled = hasMissingExtraParam
|
||||
|| isBlockedByVisitors
|
||||
|| (builderPlaceableStatus === BuilderFurniPlaceableStatus.MISSING_OFFER)
|
||||
|| (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_IN_ROOM)
|
||||
|| (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_ROOM_OWNER)
|
||||
|| (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN);
|
||||
const startBuilderPlacement = (placeMultiple: boolean) =>
|
||||
{
|
||||
if(builderPlaceableStatus === BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED)
|
||||
{
|
||||
showSingleBubble(LocalizeText('room.error.max_furniture'), NotificationBubbleType.INFO);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isDisabled) return;
|
||||
|
||||
setCatalogPlaceMultipleObjects(placeMultiple);
|
||||
requestOfferToMover(currentOffer);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 items-start">
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
<Button disabled={ isDisabled } onClick={ () => startBuilderPlacement(true) }>
|
||||
{ LocalizeText('builder.placement_widget.place_many') }
|
||||
</Button>
|
||||
<Button disabled={ isDisabled } onClick={ () => startBuilderPlacement(false) } style={ buildersClubPlaceOneButtonStyle }>
|
||||
{ LocalizeText('builder.placement_widget.place_one') }
|
||||
</Button>
|
||||
</div>
|
||||
{ isBlockedByVisitors &&
|
||||
<Text className="max-w-full" small variant="danger">
|
||||
{ LocalizeText('builder.placement_widget.error.visitors') }
|
||||
</Text> }
|
||||
{ (builderPlaceableStatus === BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN) &&
|
||||
<Text className="max-w-full" small variant="danger">
|
||||
{ LocalizeText('builder.placement_widget.error.not_group_admin') }
|
||||
</Text> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const priceCredits = (currentOffer.priceInCredits * purchaseOptions.quantity);
|
||||
const pricePoints = (currentOffer.priceInActivityPoints * purchaseOptions.quantity);
|
||||
|
||||
@@ -164,7 +237,7 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
return (
|
||||
<>
|
||||
<PurchaseButton />
|
||||
{ (!noGiftOption && !currentOffer.isRentOffer) &&
|
||||
{ (!isBuildersClubOffer && !noGiftOption && !currentOffer.isRentOffer) &&
|
||||
<Button disabled={ ((purchaseOptions.quantity > 1) || !currentOffer.giftable || isLimitedSoldOut || (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length))) } onClick={ event => purchase(true) }>
|
||||
{ LocalizeText('catalog.purchase_confirmation.gift') }
|
||||
</Button> }
|
||||
|
||||
Reference in New Issue
Block a user