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,13 +1,14 @@
|
||||
import { CatalogAdminCreateOfferComposer, CatalogAdminCreatePageComposer, CatalogAdminDeleteOfferComposer, CatalogAdminDeletePageComposer, CatalogAdminMoveOfferComposer, CatalogAdminMovePageComposer, CatalogAdminPublishComposer, CatalogAdminResultEvent, CatalogAdminSaveOfferComposer, CatalogAdminSavePageComposer } from '@nitrots/nitro-renderer';
|
||||
import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { ICatalogNode, IPurchasableOffer, NotificationAlertType, SendMessageComposer } from '../../api';
|
||||
import { useMessageEvent, useNotification } from '../../hooks';
|
||||
import { useCatalog, useMessageEvent, useNotification } from '../../hooks';
|
||||
|
||||
export interface IPageEditData
|
||||
{
|
||||
pageId?: number;
|
||||
caption: string;
|
||||
parentId: number;
|
||||
catalogMode: string;
|
||||
pageLayout: string;
|
||||
enabled: string;
|
||||
visible: string;
|
||||
@@ -75,6 +76,7 @@ export const useCatalogAdmin = () => useContext(CatalogAdminContext);
|
||||
|
||||
export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) =>
|
||||
{
|
||||
const { currentType } = useCatalog();
|
||||
const [ adminMode, setAdminMode ] = useState(false);
|
||||
const [ editingOffer, setEditingOffer ] = useState<IPurchasableOffer | null>(null);
|
||||
const [ editingPageData, setEditingPageData ] = useState(false);
|
||||
@@ -175,9 +177,9 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
data.pageId || 0, data.caption, data.caption, data.pageLayout, 0,
|
||||
data.minRank, data.visible === '1', data.enabled === '1',
|
||||
data.orderNum, data.parentId,
|
||||
data.pageHeadline || '', data.pageTeaser || '', data.pageTextDetails || ''
|
||||
data.pageHeadline || '', data.pageTeaser || '', data.pageTextDetails || '', currentType, data.catalogMode
|
||||
));
|
||||
}, []);
|
||||
}, [ currentType ]);
|
||||
|
||||
const createPage = useCallback((data: IPageEditData) =>
|
||||
{
|
||||
@@ -187,17 +189,17 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
SendMessageComposer(new CatalogAdminCreatePageComposer(
|
||||
data.caption, data.caption, data.pageLayout, 0,
|
||||
data.minRank, data.visible === '1', data.enabled === '1',
|
||||
data.orderNum, data.parentId
|
||||
data.orderNum, data.parentId, currentType, data.catalogMode
|
||||
));
|
||||
}, []);
|
||||
}, [ currentType ]);
|
||||
|
||||
const deletePage = useCallback((pageId: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setLastError(null);
|
||||
pendingActionRef.current = 'deletePage';
|
||||
SendMessageComposer(new CatalogAdminDeletePageComposer(pageId));
|
||||
}, []);
|
||||
SendMessageComposer(new CatalogAdminDeletePageComposer(pageId, currentType));
|
||||
}, [ currentType ]);
|
||||
|
||||
const saveOffer = useCallback((data: IOfferEditData) =>
|
||||
{
|
||||
@@ -208,9 +210,9 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
data.offerId || 0, data.pageId, parseInt(data.itemIds) || 0,
|
||||
data.catalogName, data.costCredits, data.costPoints, data.pointsType,
|
||||
data.amount, data.clubOnly === '1' ? 1 : 0, data.extradata,
|
||||
data.haveOffer === '1', data.offerId_group, data.limitedStack, data.orderNumber
|
||||
data.haveOffer === '1', data.offerId_group, data.limitedStack, data.orderNumber, currentType
|
||||
));
|
||||
}, []);
|
||||
}, [ currentType ]);
|
||||
|
||||
const createOffer = useCallback((data: IOfferEditData) =>
|
||||
{
|
||||
@@ -221,17 +223,17 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
data.pageId, parseInt(data.itemIds) || 0,
|
||||
data.catalogName, data.costCredits, data.costPoints, data.pointsType,
|
||||
data.amount, data.clubOnly === '1' ? 1 : 0, data.extradata,
|
||||
data.haveOffer === '1', data.offerId_group, data.limitedStack, data.orderNumber
|
||||
data.haveOffer === '1', data.offerId_group, data.limitedStack, data.orderNumber, currentType
|
||||
));
|
||||
}, []);
|
||||
}, [ currentType ]);
|
||||
|
||||
const deleteOffer = useCallback((offerId: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setLastError(null);
|
||||
pendingActionRef.current = 'deleteOffer';
|
||||
SendMessageComposer(new CatalogAdminDeleteOfferComposer(offerId));
|
||||
}, []);
|
||||
SendMessageComposer(new CatalogAdminDeleteOfferComposer(offerId, currentType));
|
||||
}, [ currentType ]);
|
||||
|
||||
const reorderOffers = useCallback((orders: { id: number; orderNumber: number }[]) =>
|
||||
{
|
||||
@@ -241,33 +243,33 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
|
||||
for(const order of orders)
|
||||
{
|
||||
SendMessageComposer(new CatalogAdminMoveOfferComposer(order.id, order.orderNumber));
|
||||
SendMessageComposer(new CatalogAdminMoveOfferComposer(order.id, order.orderNumber, currentType));
|
||||
}
|
||||
}, []);
|
||||
}, [ currentType ]);
|
||||
|
||||
const reorderPage = useCallback((pageId: number, newParentId: number, newIndex: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setLastError(null);
|
||||
pendingActionRef.current = 'movePage';
|
||||
SendMessageComposer(new CatalogAdminMovePageComposer(pageId, newParentId, newIndex));
|
||||
}, []);
|
||||
SendMessageComposer(new CatalogAdminMovePageComposer(pageId, newParentId, newIndex, currentType));
|
||||
}, [ currentType ]);
|
||||
|
||||
const togglePageEnabled = useCallback((pageId: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setLastError(null);
|
||||
pendingActionRef.current = 'toggleEnabled';
|
||||
SendMessageComposer(new CatalogAdminMovePageComposer(pageId, -1, -1));
|
||||
}, []);
|
||||
SendMessageComposer(new CatalogAdminMovePageComposer(pageId, -1, -1, currentType));
|
||||
}, [ currentType ]);
|
||||
|
||||
const togglePageVisible = useCallback((pageId: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setLastError(null);
|
||||
pendingActionRef.current = 'toggleVisible';
|
||||
SendMessageComposer(new CatalogAdminMovePageComposer(pageId, -2, -1));
|
||||
}, []);
|
||||
SendMessageComposer(new CatalogAdminMovePageComposer(pageId, -2, -1, currentType));
|
||||
}, [ currentType ]);
|
||||
|
||||
const publishCatalog = useCallback(() =>
|
||||
{
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa';
|
||||
import { GetConfigurationValue, LocalizeText } from '../../api';
|
||||
import { CatalogType, GetConfigurationValue, LocalizeText } from '../../api';
|
||||
import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useCatalog } from '../../hooks';
|
||||
import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext';
|
||||
import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView';
|
||||
import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView';
|
||||
import { CatalogBuildersClubStatusView } from './views/catalog-header/CatalogBuildersClubStatusView';
|
||||
import { CatalogIconView } from './views/catalog-icon/CatalogIconView';
|
||||
import { CatalogGiftView } from './views/gift/CatalogGiftView';
|
||||
import { CatalogNavigationView } from './views/navigation/CatalogNavigationView';
|
||||
@@ -15,7 +16,7 @@ import { MarketplacePostOfferView } from './views/page/layout/marketplace/Market
|
||||
|
||||
const CatalogClassicViewInner: FC<{}> = () =>
|
||||
{
|
||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null } = useCatalog();
|
||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {});
|
||||
@@ -24,9 +25,26 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
||||
const loading = catalogAdmin?.loading ?? false;
|
||||
|
||||
const isMod = GetSessionDataManager().isModerator;
|
||||
const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER)
|
||||
? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' }
|
||||
: undefined;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const getCatalogTypeFromLink = (type?: string) =>
|
||||
{
|
||||
switch((type || '').toLowerCase())
|
||||
{
|
||||
case 'bc':
|
||||
case 'builder':
|
||||
case 'buildersclub':
|
||||
case 'builders_club':
|
||||
return CatalogType.BUILDER;
|
||||
default:
|
||||
return CatalogType.NORMAL;
|
||||
}
|
||||
};
|
||||
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
@@ -37,12 +55,26 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
if(parts.length > 2)
|
||||
{
|
||||
openCatalogByType(getCatalogTypeFromLink(parts[2]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
if(parts.length > 2)
|
||||
{
|
||||
toggleCatalogByType(getCatalogTypeFromLink(parts[2]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsVisible(prevValue => !prevValue);
|
||||
return;
|
||||
case 'open':
|
||||
@@ -76,13 +108,13 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ setIsVisible, openPageByOfferId, openPageByName ]);
|
||||
}, [ setIsVisible, openPageByOfferId, openPageByName, openCatalogByType, toggleCatalogByType ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isVisible &&
|
||||
<NitroCardView className="w-[630px] h-[400px]" style={ GetConfigurationValue('catalog.headers') ? { width: 710 } : {} } uniqueKey="catalog">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('catalog.title') } onCloseClick={ () => setIsVisible(false) } />
|
||||
<NitroCardHeaderView className={ currentType === CatalogType.BUILDER ? 'builders-club-card-header' : '' } headerText={ LocalizeText('catalog.title') } onCloseClick={ () => setIsVisible(false) } style={ buildersClubHeaderStyle } />
|
||||
{ /* Admin banner */ }
|
||||
{ adminMode &&
|
||||
<div className="flex items-center justify-between bg-warning text-dark text-[10px] font-bold px-3 py-0.5 uppercase tracking-wider" style={ { textShadow: '0 1px 0 rgba(255,255,255,0.3)' } }>
|
||||
@@ -134,13 +166,14 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
||||
<FaCog className={ `text-[10px] ${ adminMode ? 'animate-spin' : '' }` } style={ adminMode ? { animationDuration: '3s' } : {} } />
|
||||
</NitroCardTabsItemView> }
|
||||
</NitroCardTabsView>
|
||||
<CatalogBuildersClubStatusView />
|
||||
<NitroCardContentView>
|
||||
{ /* Admin: add new root category */ }
|
||||
{ adminMode && rootNode &&
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<button
|
||||
className="flex items-center gap-1 text-[9px] text-success hover:text-green-800 cursor-pointer transition-colors"
|
||||
onClick={ () => catalogAdmin.createPage({ caption: 'New Category', pageLayout: 'default_3x3', minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
||||
onClick={ () => catalogAdmin.createPage({ caption: 'New Category', catalogMode: currentType, pageLayout: 'default_3x3', minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
||||
>
|
||||
<FaPlus className="text-[8px]" />
|
||||
<span>{ LocalizeText('catalog.admin.new') }</span>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaCog, FaEdit, FaEye, FaEyeSlash, FaHeart, FaPlus, FaStar, FaTrash } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../api';
|
||||
import { CatalogType, LocalizeText } from '../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
|
||||
import { useCatalog, useCatalogFavorites } from '../../hooks';
|
||||
import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext';
|
||||
import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView';
|
||||
import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView';
|
||||
import { CatalogBuildersClubStatusView } from './views/catalog-header/CatalogBuildersClubStatusView';
|
||||
import { CatalogIconView } from './views/catalog-icon/CatalogIconView';
|
||||
import { CatalogFavoritesView } from './views/favorites/CatalogFavoritesView';
|
||||
import { CatalogGiftView } from './views/gift/CatalogGiftView';
|
||||
@@ -17,7 +18,7 @@ import { MarketplacePostOfferView } from './views/page/layout/marketplace/Market
|
||||
|
||||
const CatalogModernViewInner: FC<{}> = () =>
|
||||
{
|
||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null } = useCatalog();
|
||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {});
|
||||
@@ -29,9 +30,26 @@ const CatalogModernViewInner: FC<{}> = () =>
|
||||
|
||||
const isMod = GetSessionDataManager().isModerator;
|
||||
const totalFavs = favoriteOfferIds.length + favoritePageIds.length;
|
||||
const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER)
|
||||
? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' }
|
||||
: undefined;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const getCatalogTypeFromLink = (type?: string) =>
|
||||
{
|
||||
switch((type || '').toLowerCase())
|
||||
{
|
||||
case 'bc':
|
||||
case 'builder':
|
||||
case 'buildersclub':
|
||||
case 'builders_club':
|
||||
return CatalogType.BUILDER;
|
||||
default:
|
||||
return CatalogType.NORMAL;
|
||||
}
|
||||
};
|
||||
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
@@ -42,12 +60,26 @@ const CatalogModernViewInner: FC<{}> = () =>
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
if(parts.length > 2)
|
||||
{
|
||||
openCatalogByType(getCatalogTypeFromLink(parts[2]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
if(parts.length > 2)
|
||||
{
|
||||
toggleCatalogByType(getCatalogTypeFromLink(parts[2]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsVisible(prevValue => !prevValue);
|
||||
return;
|
||||
case 'open':
|
||||
@@ -81,13 +113,13 @@ const CatalogModernViewInner: FC<{}> = () =>
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ setIsVisible, openPageByOfferId, openPageByName ]);
|
||||
}, [ setIsVisible, openPageByOfferId, openPageByName, openCatalogByType, toggleCatalogByType ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isVisible &&
|
||||
<NitroCardView className="nitro-catalog w-[780px] h-[520px]" uniqueKey="catalog">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('catalog.title') } onCloseClick={ () => setIsVisible(false) } />
|
||||
<NitroCardHeaderView className={ currentType === CatalogType.BUILDER ? 'builders-club-card-header' : '' } headerText={ LocalizeText('catalog.title') } onCloseClick={ () => setIsVisible(false) } style={ buildersClubHeaderStyle } />
|
||||
<NitroCardContentView classNames={ [ 'p-0!', 'overflow-hidden!' ] }>
|
||||
{ /* Admin banner */ }
|
||||
{ adminMode &&
|
||||
@@ -102,7 +134,8 @@ const CatalogModernViewInner: FC<{}> = () =>
|
||||
</button>
|
||||
</div> }
|
||||
|
||||
<div className="flex h-full">
|
||||
<CatalogBuildersClubStatusView />
|
||||
<div className="flex min-h-0 flex-1">
|
||||
{ /* === LEFT SIDEBAR === */ }
|
||||
<div className="group/rail flex flex-col w-[52px] hover:w-[175px] min-w-[52px] bg-card-grid-item border-r-2 border-card-grid-item-border py-1.5 gap-px overflow-y-auto overflow-x-hidden transition-[width] duration-200 ease-in-out">
|
||||
|
||||
@@ -129,7 +162,7 @@ const CatalogModernViewInner: FC<{}> = () =>
|
||||
<button
|
||||
className="flex items-center gap-1 text-[9px] text-success hover:text-green-800 cursor-pointer transition-colors"
|
||||
title={ LocalizeText('catalog.admin.new.root.category') }
|
||||
onClick={ () => catalogAdmin.createPage({ caption: 'New Category', pageLayout: 'default_3x3', minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
||||
onClick={ () => catalogAdmin.createPage({ caption: 'New Category', catalogMode: currentType, pageLayout: 'default_3x3', minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
||||
>
|
||||
<FaPlus className="text-[8px]" />
|
||||
<span className="whitespace-nowrap">{ LocalizeText('catalog.admin.new') }</span>
|
||||
|
||||
@@ -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