mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
feat: add builders club catalog ui flow
This commit is contained in:
@@ -7,4 +7,5 @@ export class BuilderFurniPlaceableStatus
|
||||
public static NOT_ROOM_OWNER: number = 4;
|
||||
public static GUILD_ROOM: number = 5;
|
||||
public static VISITORS_IN_ROOM: number = 6;
|
||||
public static NOT_GROUP_ADMIN: number = 7;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ICatalogOptions
|
||||
groups?: HabboGroupEntryData[];
|
||||
petPalettes?: CatalogPetPalette[];
|
||||
clubOffers?: ClubOfferData[];
|
||||
clubOffersByWindowId?: Record<number, ClubOfferData[]>;
|
||||
clubGifts?: ClubGiftInfoParser;
|
||||
giftConfiguration?: GiftWrappingConfiguration;
|
||||
marketplaceConfiguration?: MarketplaceConfigurationMessageParser;
|
||||
|
||||
@@ -42,6 +42,8 @@ export class FriendlyTime
|
||||
|
||||
public static getLocalization(key: string, amount: number): string
|
||||
{
|
||||
return LocalizeText(key, [ 'amount' ], [ amount.toString() ]);
|
||||
const amountValue = amount.toString();
|
||||
|
||||
return LocalizeText(key, [ 'amount', 'AMOUNT' ], [ amountValue, amountValue ]);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -24,6 +24,8 @@ export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttrib
|
||||
export const Base: FC<BaseProps<HTMLDivElement>> = props =>
|
||||
{
|
||||
const { ref = null, innerRef = null, display = null, fit = false, fitV = false, grow = false, shrink = false, fullWidth = false, fullHeight = false, overflow = null, position = null, float = null, pointer = false, visible = null, textColor = null, classNames = [], className = '', style = {}, children = null, ...rest } = props;
|
||||
const safeClassNames = Array.isArray(classNames) ? classNames : [];
|
||||
const safeClassName = (typeof className === 'string') ? className : '';
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
@@ -53,19 +55,19 @@ export const Base: FC<BaseProps<HTMLDivElement>> = props =>
|
||||
|
||||
if(textColor) newClassNames.push('text-' + textColor);
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
if(safeClassNames.length) newClassNames.push(...safeClassNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ display, fit, fitV, grow, shrink, fullWidth, fullHeight, overflow, position, float, pointer, visible, textColor, classNames ]);
|
||||
}, [ display, fit, fitV, grow, shrink, fullWidth, fullHeight, overflow, position, float, pointer, visible, textColor, safeClassNames ]);
|
||||
|
||||
const getClassName = useMemo(() =>
|
||||
{
|
||||
let newClassName = getClassNames.join(' ');
|
||||
|
||||
if(className.length) newClassName += (' ' + className);
|
||||
if(safeClassName.length) newClassName += (' ' + safeClassName);
|
||||
|
||||
return newClassName.trim();
|
||||
}, [ getClassNames, className ]);
|
||||
}, [ getClassNames, safeClassName ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
|
||||
@@ -15,9 +15,7 @@ interface NitroCardHeaderViewProps extends ColumnProps
|
||||
|
||||
export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
|
||||
{
|
||||
const { headerText = null, isGalleryPhoto = false, noCloseButton = false, isInfoToHabboPages = false, onReportPhoto = null, onClickInfoHabboPages = null, onCloseClick = null, justifyContent = 'center', alignItems = 'center', classNames = [], children = null, ...rest } = props;
|
||||
|
||||
|
||||
const { headerText = null, isGalleryPhoto = false, noCloseButton = false, isInfoToHabboPages = false, onReportPhoto = null, onClickInfoHabboPages = null, onCloseClick = null, justifyContent = 'center', alignItems = 'center', classNames = [], className = '', children = null, ...rest } = props;
|
||||
|
||||
const onMouseDown = (event: MouseEvent<HTMLDivElement>) =>
|
||||
{
|
||||
@@ -26,7 +24,11 @@ export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
|
||||
};
|
||||
|
||||
return (
|
||||
<Column center className={ 'nitro-card-header-shell relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header' } { ...rest }>
|
||||
<Column
|
||||
center
|
||||
classNames={ [ 'nitro-card-header-shell', 'relative', 'flex', 'items-center', 'justify-center', 'flex-col', 'drag-handler', 'min-h-card-header', 'max-h-card-header', ...classNames ] }
|
||||
className={ className }
|
||||
{ ...rest }>
|
||||
<Flex center fullWidth>
|
||||
<span className="nitro-card-title text-white">{ headerText }</span>
|
||||
{ isGalleryPhoto &&
|
||||
|
||||
@@ -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> }
|
||||
|
||||
@@ -96,7 +96,7 @@ export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
|
||||
<div className="text-white font-bold text-[13px] drop-shadow-[1px_1px_0_#000] truncate max-w-[120px] px-1">{friend.name}</div>
|
||||
<div className="flex justify-center gap-3 px-2">
|
||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-chat hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); OpenMessengerChat(friend.id); setVisible(false); }} />
|
||||
{friend.followingAllowed &&
|
||||
{friend.online &&
|
||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-visit hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); followFriend(friend); setVisible(false); }} />}
|
||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-profile hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); GetUserProfile(friend.id); setVisible(false); }} />
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ export const FriendsListGroupItemView: FC<{ friend: MessengerFriend, selected: b
|
||||
<div className="flex items-center gap-1">
|
||||
{ !isRelationshipOpen &&
|
||||
<>
|
||||
{ friend.followingAllowed &&
|
||||
{ friend.online &&
|
||||
<div className="nitro-friends-spritesheet icon-follow cursor-pointer" title={ LocalizeText('friendlist.tip.follow') } onClick={ clickFollowFriend } /> }
|
||||
{ friend.online &&
|
||||
<div className="nitro-friends-spritesheet icon-chat cursor-pointer" title={ LocalizeText('friendlist.tip.im') } onClick={ openMessengerChat } /> }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CrackableDataType, CreateLinkEvent, FurnitureFloorUpdateEvent, GetRoomEngine, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType, UpdateFurniturePositionComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaCrosshairs, FaRulerVertical, FaTimes } from 'react-icons/fa';
|
||||
import { GrFormNextLink, GrRotateLeft, GrRotateRight } from 'react-icons/gr';
|
||||
import { AvatarInfoFurni, GetGroupInformation, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
@@ -45,6 +45,12 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
const [ itemLocation, setItemLocation ] = useState<{ x: number; y: number; z: number }>({ x: -1, y: -1, z: -1 });
|
||||
const [ dropdownOpen, setDropdownOpen ] = useState(sessionStorage.getItem('dropdownOpen') === 'true');
|
||||
const [ furniLocationZ, setFurniLocationZ ] = useState<number>(null);
|
||||
const showOwnerProfileIcon = useMemo(() =>
|
||||
{
|
||||
const ownerName = (avatarInfo?.ownerName || '').trim().toLowerCase();
|
||||
|
||||
return !!avatarInfo && (avatarInfo.ownerId > 0) && (ownerName !== 'builders club');
|
||||
}, [ avatarInfo ]);
|
||||
|
||||
const sendUpdate = useCallback((deltaX: number, deltaY: number, newZ: number = 0, deltaDirection: number = 0) =>
|
||||
{
|
||||
@@ -490,7 +496,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={ avatarInfo.ownerId } />
|
||||
{ showOwnerProfileIcon && <UserProfileIconView userId={ avatarInfo.ownerId } /> }
|
||||
<Text small wrap variant="white">
|
||||
{ LocalizeText('furni.owner', [ 'name' ], [ avatarInfo.ownerName ]) }
|
||||
</Text>
|
||||
|
||||
@@ -140,7 +140,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<ToolbarItemView icon="game" onClick={ () => CreateLinkEvent('games/toggle') } className="tb-icon" />
|
||||
</motion.div> }
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle') } className="tb-icon" />
|
||||
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" />
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<ToolbarItemView icon="inventory" onClick={ () => CreateLinkEvent('inventory/toggle') } className="tb-icon" />
|
||||
@@ -227,7 +230,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<ToolbarItemView icon="game" onClick={ () => CreateLinkEvent('games/toggle') } className="tb-icon" />
|
||||
</motion.div> }
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle') } className="tb-icon" />
|
||||
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" />
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<ToolbarItemView icon="inventory" onClick={ () => CreateLinkEvent('inventory/toggle') } className="tb-icon" />
|
||||
|
||||
@@ -36,6 +36,13 @@
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.nitro-icon.icon-buildersclub {
|
||||
background-image: url("@/assets/images/toolbar/icons/buildersclub.png");
|
||||
background-size: contain;
|
||||
width: 38px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.nitro-icon.icon-game {
|
||||
background-image: url("@/assets/images/toolbar/icons/game.png");
|
||||
width: 44px;
|
||||
|
||||
@@ -200,6 +200,12 @@ body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.nitro-card-header-shell.builders-club-card-header {
|
||||
border-color: #d79d2e;
|
||||
border-bottom-color: #000;
|
||||
background: linear-gradient(180deg, #d89f2d 0%, #c68515 100%);
|
||||
}
|
||||
|
||||
.nitro-card-title {
|
||||
margin: 0 auto;
|
||||
font-family: UbuntuCondensed, Ubuntu, sans-serif;
|
||||
@@ -207,6 +213,18 @@ body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.builders-club-status-shell {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-bottom: 2px solid #000;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(84, 62, 46, 0.95) 0%, rgba(50, 38, 29, 0.97) 100%);
|
||||
}
|
||||
|
||||
.builders-club-status-icon-shell {
|
||||
background: rgba(255, 193, 70, 0.12);
|
||||
border: 1px solid rgba(255, 193, 70, 0.26);
|
||||
}
|
||||
|
||||
.nitro-card-close-button,
|
||||
.ubuntu-close-button {
|
||||
width: 20px;
|
||||
@@ -377,6 +395,10 @@ body {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.builders-club-status-shell {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.nitro-card-title {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
+135
-29
@@ -1,4 +1,4 @@
|
||||
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, ClubGiftInfoEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetGiftWrappingConfigurationComposer, GetRoomEngine, GetTickerTime, GiftWrappingConfigurationEvent, GuildMembershipsMessageEvent, HabboClubOffersMessageEvent, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, SellablePetPalettesMessageEvent, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, ClubGiftInfoEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetGiftWrappingConfigurationComposer, GetRoomEngine, GetSessionDataManager, GetTickerTime, GiftWrappingConfigurationEvent, GuildMembershipsMessageEvent, HabboClubOffersMessageEvent, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, SellablePetPalettesMessageEvent, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { BuilderFurniPlaceableStatus, CatalogNode, CatalogPage, CatalogPetPalette, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, GiftWrappingConfiguration, ICatalogNode, ICatalogOptions, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api';
|
||||
@@ -40,7 +40,10 @@ const useCatalogState = () =>
|
||||
const [ secondsLeft, setSecondsLeft ] = useState(0);
|
||||
const [ updateTime, setUpdateTime ] = useState(0);
|
||||
const [ secondsLeftWithGrace, setSecondsLeftWithGrace ] = useState(0);
|
||||
const { simpleAlert = null } = useNotification();
|
||||
const [ builderPlacementBlockedByVisitors, setBuilderPlacementBlockedByVisitors ] = useState(false);
|
||||
const [ builderPlacementAllowedInCurrentRoom, setBuilderPlacementAllowedInCurrentRoom ] = useState(false);
|
||||
const [ builderTrialRoomHideConfirmed, setBuilderTrialRoomHideConfirmed ] = useState(false);
|
||||
const { simpleAlert = null, showConfirm = null } = useNotification();
|
||||
const requestedPage = useRef(new RequestedPage());
|
||||
|
||||
const resetState = useCallback(() =>
|
||||
@@ -57,37 +60,108 @@ const useCatalogState = () =>
|
||||
setIsVisible(false);
|
||||
}, []);
|
||||
|
||||
const normalizeCatalogType = useCallback((type?: string) =>
|
||||
{
|
||||
if(type === CatalogType.BUILDER) return CatalogType.BUILDER;
|
||||
|
||||
return CatalogType.NORMAL;
|
||||
}, []);
|
||||
|
||||
const resetVisibleCatalogState = useCallback((type?: string) =>
|
||||
{
|
||||
requestedPage.current.resetRequest();
|
||||
|
||||
setPageId(-1);
|
||||
setPreviousPageId(-1);
|
||||
setRootNode(null);
|
||||
setOffersToNodes(null);
|
||||
setCurrentPage(null);
|
||||
setCurrentOffer(null);
|
||||
setActiveNodes([]);
|
||||
setSearchResult(null);
|
||||
setFrontPageItems([]);
|
||||
setNavigationHidden(false);
|
||||
setCurrentType(normalizeCatalogType(type));
|
||||
}, [ normalizeCatalogType ]);
|
||||
|
||||
const openCatalogByType = useCallback((type?: string) =>
|
||||
{
|
||||
const catalogType = normalizeCatalogType(type);
|
||||
|
||||
if(currentType !== catalogType)
|
||||
{
|
||||
resetVisibleCatalogState(catalogType);
|
||||
}
|
||||
|
||||
setIsVisible(true);
|
||||
}, [ currentType, normalizeCatalogType, resetVisibleCatalogState ]);
|
||||
|
||||
const toggleCatalogByType = useCallback((type?: string) =>
|
||||
{
|
||||
const catalogType = normalizeCatalogType(type);
|
||||
|
||||
if(isVisible && (currentType === catalogType))
|
||||
{
|
||||
setIsVisible(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentType !== catalogType)
|
||||
{
|
||||
resetVisibleCatalogState(catalogType);
|
||||
}
|
||||
|
||||
setIsVisible(true);
|
||||
}, [ isVisible, currentType, normalizeCatalogType, resetVisibleCatalogState ]);
|
||||
|
||||
const getBuilderFurniPlaceableStatus = useCallback((offer: IPurchasableOffer) =>
|
||||
{
|
||||
if(!offer) return BuilderFurniPlaceableStatus.MISSING_OFFER;
|
||||
|
||||
if((furniCount < 0) || (furniCount >= furniLimit)) return BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED;
|
||||
|
||||
const roomSession = GetRoomSession();
|
||||
const canUseGuildAdminFallback = (!!roomSession
|
||||
&& roomSession.isGuildRoom
|
||||
&& (roomSession.controllerLevel >= RoomControllerLevel.GUILD_ADMIN)
|
||||
&& (secondsLeft > 0));
|
||||
const usesSharedPlacementPool = (!!roomSession && !roomSession.isRoomOwner && (builderPlacementAllowedInCurrentRoom || canUseGuildAdminFallback));
|
||||
|
||||
if(!roomSession) return BuilderFurniPlaceableStatus.NOT_IN_ROOM;
|
||||
|
||||
if(!roomSession.isRoomOwner) return BuilderFurniPlaceableStatus.NOT_ROOM_OWNER;
|
||||
if(!roomSession.isRoomOwner && !builderPlacementAllowedInCurrentRoom && !canUseGuildAdminFallback) return BuilderFurniPlaceableStatus.NOT_GROUP_ADMIN;
|
||||
|
||||
if(!usesSharedPlacementPool && ((furniCount < 0) || (furniCount >= furniLimit))) return BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED;
|
||||
|
||||
if((secondsLeft <= 0) && builderPlacementBlockedByVisitors) return BuilderFurniPlaceableStatus.VISITORS_IN_ROOM;
|
||||
|
||||
if(secondsLeft <= 0)
|
||||
{
|
||||
const roomEngine = GetRoomEngine();
|
||||
const userDataManager = roomSession.userDataManager;
|
||||
const sessionDataManager = GetSessionDataManager();
|
||||
|
||||
let objectCount = roomEngine.getRoomObjectCount(roomSession.roomId, RoomObjectCategory.UNIT);
|
||||
if(!roomEngine || !userDataManager || !sessionDataManager) return BuilderFurniPlaceableStatus.OKAY;
|
||||
|
||||
while(objectCount > 0)
|
||||
const roomObjects = roomEngine.getRoomObjects(roomSession.roomId, RoomObjectCategory.UNIT);
|
||||
|
||||
if(!roomObjects || !roomObjects.length) return BuilderFurniPlaceableStatus.OKAY;
|
||||
|
||||
for(const roomObject of roomObjects)
|
||||
{
|
||||
const roomObject = roomEngine.getRoomObjectByIndex(roomSession.roomId, objectCount, RoomObjectCategory.UNIT);
|
||||
const userData = roomSession.userDataManager.getUserDataByIndex(roomObject.id);
|
||||
if(!roomObject) continue;
|
||||
|
||||
if(userData && (userData.type === RoomObjectType.USER) && (userData.roomIndex !== roomSession.ownRoomIndex) && !userData.isModerator) return BuilderFurniPlaceableStatus.VISITORS_IN_ROOM;
|
||||
const userData = userDataManager.getUserDataByIndex(roomObject.id);
|
||||
|
||||
objectCount--;
|
||||
if(!userData || (userData.type !== RoomObjectType.USER)) continue;
|
||||
if(userData.webID === sessionDataManager.userId) continue;
|
||||
if(userData.isModerator) continue;
|
||||
|
||||
return BuilderFurniPlaceableStatus.VISITORS_IN_ROOM;
|
||||
}
|
||||
}
|
||||
|
||||
return BuilderFurniPlaceableStatus.OKAY;
|
||||
}, [ furniCount, furniLimit, secondsLeft ]);
|
||||
}, [ builderPlacementAllowedInCurrentRoom, builderPlacementBlockedByVisitors, furniCount, furniLimit, secondsLeft ]);
|
||||
|
||||
const isDraggable = useCallback((offer: IPurchasableOffer) =>
|
||||
{
|
||||
@@ -419,6 +493,10 @@ const useCatalogState = () =>
|
||||
useMessageEvent<CatalogPagesListEvent>(CatalogPagesListEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const parserCatalogType = normalizeCatalogType(parser.catalogType);
|
||||
|
||||
if(parserCatalogType !== currentType) return;
|
||||
|
||||
const offers: Map<number, ICatalogNode[]> = new Map();
|
||||
|
||||
const getCatalogNode = (node: NodeData, depth: number, parent: ICatalogNode) =>
|
||||
@@ -589,9 +667,14 @@ const useCatalogState = () =>
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const clubOffers = parser.offers;
|
||||
const windowId = parser.windowId;
|
||||
const clubOffersByWindowId = { ...(prevValue.clubOffersByWindowId || {}) };
|
||||
|
||||
return { ...prevValue, clubOffers };
|
||||
clubOffersByWindowId[windowId] = parser.offers;
|
||||
|
||||
const clubOffers = clubOffersByWindowId[1] || prevValue.clubOffers;
|
||||
|
||||
return { ...prevValue, clubOffers, clubOffersByWindowId };
|
||||
});
|
||||
});
|
||||
|
||||
@@ -679,6 +762,8 @@ const useCatalogState = () =>
|
||||
setSecondsLeft(parser.secondsLeft);
|
||||
setUpdateTime(GetTickerTime());
|
||||
setSecondsLeftWithGrace(parser.secondsLeftWithGrace);
|
||||
setBuilderPlacementBlockedByVisitors(parser.placementBlockedByVisitors);
|
||||
setBuilderPlacementAllowedInCurrentRoom(parser.placementAllowedInCurrentRoom);
|
||||
|
||||
refreshBuilderStatus();
|
||||
});
|
||||
@@ -772,24 +857,40 @@ const useCatalogState = () =>
|
||||
break;
|
||||
}
|
||||
case CatalogType.BUILDER: {
|
||||
let pageId = purchasableOffer.page.pageId;
|
||||
|
||||
if(pageId === DUMMY_PAGE_ID_FOR_OFFER_SEARCH)
|
||||
const placeBuilderItem = () =>
|
||||
{
|
||||
pageId = -1;
|
||||
}
|
||||
let pageId = purchasableOffer.page.pageId;
|
||||
|
||||
switch(event.category)
|
||||
if(pageId === DUMMY_PAGE_ID_FOR_OFFER_SEARCH)
|
||||
{
|
||||
pageId = -1;
|
||||
}
|
||||
|
||||
switch(event.category)
|
||||
{
|
||||
case RoomObjectCategory.FLOOR:
|
||||
SendMessageComposer(new BuildersClubPlaceRoomItemMessageComposer(pageId, purchasableOffer.offerId, product.extraParam, event.x, event.y, event.direction));
|
||||
break;
|
||||
case RoomObjectCategory.WALL:
|
||||
SendMessageComposer(new BuildersClubPlaceWallItemMessageComposer(pageId, purchasableOffer.offerId, product.extraParam, event.wallLocation));
|
||||
break;
|
||||
}
|
||||
|
||||
if(catalogPlaceMultipleObjects && ((furniCount + 1) < furniLimit)) requestOfferToMover(purchasableOffer);
|
||||
};
|
||||
|
||||
if((secondsLeft <= 0) && (furniCount <= 0) && !builderTrialRoomHideConfirmed && showConfirm)
|
||||
{
|
||||
case RoomObjectCategory.FLOOR:
|
||||
SendMessageComposer(new BuildersClubPlaceRoomItemMessageComposer(pageId, purchasableOffer.offerId, product.extraParam, event.x, event.y, event.direction));
|
||||
break;
|
||||
case RoomObjectCategory.WALL:
|
||||
SendMessageComposer(new BuildersClubPlaceWallItemMessageComposer(pageId, purchasableOffer.offerId, product.extraParam, event.wallLocation));
|
||||
break;
|
||||
showConfirm(LocalizeText('room.confirm.hide_room'), () =>
|
||||
{
|
||||
setBuilderTrialRoomHideConfirmed(true);
|
||||
placeBuilderItem();
|
||||
}, () => resetPlacedOfferData());
|
||||
}
|
||||
else
|
||||
{
|
||||
placeBuilderItem();
|
||||
}
|
||||
|
||||
if(catalogPlaceMultipleObjects) requestOfferToMover(purchasableOffer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -882,6 +983,11 @@ const useCatalogState = () =>
|
||||
setPurchaseOptions({ quantity: 1, extraData: null, extraParamRequired: false, previewStuffData: null });
|
||||
}, [ currentOffer ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(secondsLeft > 0) setBuilderTrialRoomHideConfirmed(false);
|
||||
}, [ secondsLeft ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible || rootNode) return;
|
||||
@@ -907,7 +1013,7 @@ const useCatalogState = () =>
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isVisible, setIsVisible, isBusy, pageId, previousPageId, currentType, rootNode, offersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, activeNodes, searchResult, setSearchResult, frontPageItems, roomPreviewer, navigationHidden, setNavigationHidden, purchaseOptions, setPurchaseOptions, catalogOptions, setCatalogOptions, getNodeById, getNodeByName, activateNode, openPageById, openPageByName, openPageByOfferId, requestOfferToMover };
|
||||
return { isVisible, setIsVisible, isBusy, pageId, previousPageId, currentType, rootNode, offersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, activeNodes, searchResult, setSearchResult, frontPageItems, roomPreviewer, navigationHidden, setNavigationHidden, purchaseOptions, setPurchaseOptions, catalogOptions, setCatalogOptions, getNodeById, getNodeByName, activateNode, openPageById, openPageByName, openPageByOfferId, requestOfferToMover, openCatalogByType, toggleCatalogByType, furniCount, furniLimit, maxFurniLimit, secondsLeft, secondsLeftWithGrace, updateTime, catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects, getBuilderFurniPlaceableStatus };
|
||||
};
|
||||
|
||||
export const useCatalog = () => useBetween(useCatalogState);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { CatalogType } from '../../api';
|
||||
import { useCatalog } from './useCatalog';
|
||||
|
||||
export interface IFavoriteOffer
|
||||
{
|
||||
@@ -8,15 +10,22 @@ export interface IFavoriteOffer
|
||||
iconUrl?: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY_OFFERS = 'catalog_fav_offers_v2';
|
||||
const STORAGE_KEY_PAGES = 'catalog_fav_pages';
|
||||
const LEGACY_STORAGE_KEY_OFFERS = 'catalog_fav_offers_v2';
|
||||
const LEGACY_STORAGE_KEY_PAGES = 'catalog_fav_pages';
|
||||
const STORAGE_KEY_OFFERS_NORMAL = 'catalog_fav_offers_v3_normal';
|
||||
const STORAGE_KEY_OFFERS_BUILDER = 'catalog_fav_offers_v3_builder';
|
||||
const STORAGE_KEY_PAGES_NORMAL = 'catalog_fav_pages_v2_normal';
|
||||
const STORAGE_KEY_PAGES_BUILDER = 'catalog_fav_pages_v2_builder';
|
||||
|
||||
const readOffers = (): IFavoriteOffer[] =>
|
||||
const normalizeCatalogType = (catalogType?: string) => ((catalogType === CatalogType.BUILDER) ? CatalogType.BUILDER : CatalogType.NORMAL);
|
||||
|
||||
const getOffersStorageKey = (catalogType?: string) => ((normalizeCatalogType(catalogType) === CatalogType.BUILDER) ? STORAGE_KEY_OFFERS_BUILDER : STORAGE_KEY_OFFERS_NORMAL);
|
||||
const getPagesStorageKey = (catalogType?: string) => ((normalizeCatalogType(catalogType) === CatalogType.BUILDER) ? STORAGE_KEY_PAGES_BUILDER : STORAGE_KEY_PAGES_NORMAL);
|
||||
|
||||
const parseOffers = (raw: string): IFavoriteOffer[] =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const raw = localStorage.getItem(STORAGE_KEY_OFFERS);
|
||||
if(!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
if(!Array.isArray(parsed)) return [];
|
||||
|
||||
@@ -34,12 +43,10 @@ const readOffers = (): IFavoriteOffer[] =>
|
||||
}
|
||||
};
|
||||
|
||||
const readPages = (): number[] =>
|
||||
const parsePages = (raw: string): number[] =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const raw = localStorage.getItem(STORAGE_KEY_PAGES);
|
||||
if(!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
}
|
||||
@@ -49,28 +56,92 @@ const readPages = (): number[] =>
|
||||
}
|
||||
};
|
||||
|
||||
const writeOffers = (offers: IFavoriteOffer[]) =>
|
||||
const readOffers = (catalogType?: string): IFavoriteOffer[] =>
|
||||
{
|
||||
localStorage.setItem(STORAGE_KEY_OFFERS, JSON.stringify(offers));
|
||||
const storageKey = getOffersStorageKey(catalogType);
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
|
||||
if(raw) return parseOffers(raw);
|
||||
|
||||
if(normalizeCatalogType(catalogType) === CatalogType.NORMAL)
|
||||
{
|
||||
const legacyRaw = localStorage.getItem(LEGACY_STORAGE_KEY_OFFERS);
|
||||
|
||||
if(legacyRaw)
|
||||
{
|
||||
const migrated = parseOffers(legacyRaw);
|
||||
|
||||
localStorage.setItem(storageKey, JSON.stringify(migrated));
|
||||
|
||||
return migrated;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const writePages = (ids: number[]) =>
|
||||
const readPages = (catalogType?: string): number[] =>
|
||||
{
|
||||
localStorage.setItem(STORAGE_KEY_PAGES, JSON.stringify(ids));
|
||||
const storageKey = getPagesStorageKey(catalogType);
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
|
||||
if(raw) return parsePages(raw);
|
||||
|
||||
if(normalizeCatalogType(catalogType) === CatalogType.NORMAL)
|
||||
{
|
||||
const legacyRaw = localStorage.getItem(LEGACY_STORAGE_KEY_PAGES);
|
||||
|
||||
if(legacyRaw)
|
||||
{
|
||||
const migrated = parsePages(legacyRaw);
|
||||
|
||||
localStorage.setItem(storageKey, JSON.stringify(migrated));
|
||||
|
||||
return migrated;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const writeOffers = (catalogType: string, offers: IFavoriteOffer[]) =>
|
||||
{
|
||||
localStorage.setItem(getOffersStorageKey(catalogType), JSON.stringify(offers));
|
||||
};
|
||||
|
||||
const writePages = (catalogType: string, ids: number[]) =>
|
||||
{
|
||||
localStorage.setItem(getPagesStorageKey(catalogType), JSON.stringify(ids));
|
||||
};
|
||||
|
||||
const useCatalogFavoritesState = () =>
|
||||
{
|
||||
const [ favoriteOffers, setFavoriteOffers ] = useState<IFavoriteOffer[]>([]);
|
||||
const [ favoritePageIds, setFavoritePageIds ] = useState<number[]>([]);
|
||||
const { currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const catalogType = normalizeCatalogType(currentType);
|
||||
const [ favoriteOffersByType, setFavoriteOffersByType ] = useState<Record<string, IFavoriteOffer[]>>({
|
||||
[CatalogType.NORMAL]: [],
|
||||
[CatalogType.BUILDER]: []
|
||||
});
|
||||
const [ favoritePageIdsByType, setFavoritePageIdsByType ] = useState<Record<string, number[]>>({
|
||||
[CatalogType.NORMAL]: [],
|
||||
[CatalogType.BUILDER]: []
|
||||
});
|
||||
const [ loaded, setLoaded ] = useState(false);
|
||||
const favoriteOffers = favoriteOffersByType[catalogType] || [];
|
||||
const favoritePageIds = favoritePageIdsByType[catalogType] || [];
|
||||
|
||||
const favoriteOfferIds = favoriteOffers.map(f => f.offerId);
|
||||
|
||||
const loadFavorites = useCallback(() =>
|
||||
{
|
||||
setFavoriteOffers(readOffers());
|
||||
setFavoritePageIds(readPages());
|
||||
setFavoriteOffersByType({
|
||||
[CatalogType.NORMAL]: readOffers(CatalogType.NORMAL),
|
||||
[CatalogType.BUILDER]: readOffers(CatalogType.BUILDER)
|
||||
});
|
||||
setFavoritePageIdsByType({
|
||||
[CatalogType.NORMAL]: readPages(CatalogType.NORMAL),
|
||||
[CatalogType.BUILDER]: readPages(CatalogType.BUILDER)
|
||||
});
|
||||
setLoaded(true);
|
||||
}, []);
|
||||
|
||||
@@ -81,32 +152,37 @@ const useCatalogFavoritesState = () =>
|
||||
|
||||
const toggleFavoriteOffer = useCallback((offerId: number, name?: string, iconUrl?: string) =>
|
||||
{
|
||||
setFavoriteOffers(prev =>
|
||||
setFavoriteOffersByType(prev =>
|
||||
{
|
||||
const exists = prev.find(f => f.offerId === offerId);
|
||||
const currentOffers = prev[catalogType] || [];
|
||||
const exists = currentOffers.find(f => f.offerId === offerId);
|
||||
|
||||
if(exists)
|
||||
{
|
||||
const next = prev.filter(f => f.offerId !== offerId);
|
||||
writeOffers(next);
|
||||
return next;
|
||||
const next = currentOffers.filter(f => f.offerId !== offerId);
|
||||
writeOffers(catalogType, next);
|
||||
|
||||
return { ...prev, [catalogType]: next };
|
||||
}
|
||||
|
||||
const next = [ ...prev, { offerId, name, iconUrl } ];
|
||||
writeOffers(next);
|
||||
return next;
|
||||
const next = [ ...currentOffers, { offerId, name, iconUrl } ];
|
||||
writeOffers(catalogType, next);
|
||||
|
||||
return { ...prev, [catalogType]: next };
|
||||
});
|
||||
}, []);
|
||||
}, [ catalogType ]);
|
||||
|
||||
const toggleFavoritePage = useCallback((pageId: number) =>
|
||||
{
|
||||
setFavoritePageIds(prev =>
|
||||
setFavoritePageIdsByType(prev =>
|
||||
{
|
||||
const next = prev.includes(pageId) ? prev.filter(id => id !== pageId) : [ ...prev, pageId ];
|
||||
writePages(next);
|
||||
return next;
|
||||
const currentPages = prev[catalogType] || [];
|
||||
const next = currentPages.includes(pageId) ? currentPages.filter(id => id !== pageId) : [ ...currentPages, pageId ];
|
||||
writePages(catalogType, next);
|
||||
|
||||
return { ...prev, [catalogType]: next };
|
||||
});
|
||||
}, []);
|
||||
}, [ catalogType ]);
|
||||
|
||||
const isFavoriteOffer = useCallback((offerId: number) =>
|
||||
{
|
||||
@@ -123,7 +199,7 @@ const useCatalogFavoritesState = () =>
|
||||
return favoriteOffers.find(f => f.offerId === offerId);
|
||||
}, [ favoriteOffers ]);
|
||||
|
||||
return { favoriteOffers, favoriteOfferIds, favoritePageIds, loaded, loadFavorites, toggleFavoriteOffer, toggleFavoritePage, isFavoriteOffer, isFavoritePage, getFavoriteOffer };
|
||||
return { favoriteOffers, favoriteOfferIds, favoritePageIds, loaded, loadFavorites, toggleFavoriteOffer, toggleFavoritePage, isFavoriteOffer, isFavoritePage, getFavoriteOffer, catalogType };
|
||||
};
|
||||
|
||||
export const useCatalogFavorites = () => useBetween(useCatalogFavoritesState);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { AcceptFriendMessageComposer, DeclineFriendMessageComposer, FollowFriendMessageComposer, FriendListFragmentEvent, FriendListUpdateComposer, FriendListUpdateEvent, FriendParser, FriendRequestsEvent, GetFriendRequestsComposer, GetSessionDataManager, MessengerInitComposer, MessengerInitEvent, NewFriendRequestEvent, RequestFriendComposer, SetRelationshipStatusComposer } from '@nitrots/nitro-renderer';
|
||||
import { AcceptFriendMessageComposer, DeclineFriendMessageComposer, FollowFriendFailedEvent, FollowFriendMessageComposer, FriendListFragmentEvent, FriendListUpdateComposer, FriendListUpdateEvent, FriendParser, FriendRequestsEvent, GetFriendRequestsComposer, GetSessionDataManager, MessengerInitComposer, MessengerInitEvent, NewFriendRequestEvent, RequestFriendComposer, SetRelationshipStatusComposer } from '@nitrots/nitro-renderer';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { CloneObject, MessengerFriend, MessengerRequest, MessengerSettings, SendMessageComposer } from '../../api';
|
||||
import { CloneObject, LocalizeText, MessengerFriend, MessengerRequest, MessengerSettings, NotificationAlertType, SendMessageComposer } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
import { useNotification } from '../notification';
|
||||
|
||||
const useFriendsState = () =>
|
||||
{
|
||||
@@ -11,6 +12,7 @@ const useFriendsState = () =>
|
||||
const [ sentRequests, setSentRequests ] = useState<number[]>([]);
|
||||
const [ dismissedRequestIds, setDismissedRequestIds ] = useState<number[]>([]);
|
||||
const [ settings, setSettings ] = useState<MessengerSettings>(null);
|
||||
const { simpleAlert = null } = useNotification();
|
||||
|
||||
const onlineFriends = useMemo(() =>
|
||||
{
|
||||
@@ -152,17 +154,16 @@ const useFriendsState = () =>
|
||||
const processUpdate = (friend: FriendParser) =>
|
||||
{
|
||||
const index = newValue.findIndex(existingFriend => (existingFriend.id === friend.id));
|
||||
const newFriend = new MessengerFriend();
|
||||
newFriend.populate(friend);
|
||||
|
||||
if(index === -1)
|
||||
{
|
||||
const newFriend = new MessengerFriend();
|
||||
newFriend.populate(friend);
|
||||
|
||||
newValue.unshift(newFriend);
|
||||
}
|
||||
else
|
||||
{
|
||||
newValue[index].populate(friend);
|
||||
newValue[index] = newFriend;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -211,6 +212,11 @@ const useFriendsState = () =>
|
||||
});
|
||||
});
|
||||
|
||||
useMessageEvent<FollowFriendFailedEvent>(FollowFriendFailedEvent, () =>
|
||||
{
|
||||
simpleAlert(LocalizeText('friendlist.followerror.hotelview'), NotificationAlertType.DEFAULT, null, null, LocalizeText('friendlist.alert.title'));
|
||||
});
|
||||
|
||||
useMessageEvent<NewFriendRequestEvent>(NewFriendRequestEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
@@ -73,7 +73,17 @@ const useNotificationState = () =>
|
||||
|
||||
const notificationItem = new NotificationBubbleItem(message, type, imageUrl, internalLink);
|
||||
|
||||
setBubbleAlerts(prevValue => [ notificationItem, ...prevValue ]);
|
||||
setBubbleAlerts(prevValue =>
|
||||
{
|
||||
if(type === NotificationBubbleType.CLUBGIFT)
|
||||
{
|
||||
const filteredAlerts = prevValue.filter(value => (value.notificationType !== NotificationBubbleType.CLUBGIFT));
|
||||
|
||||
return [ notificationItem, ...filteredAlerts ];
|
||||
}
|
||||
|
||||
return [ notificationItem, ...prevValue ];
|
||||
});
|
||||
}, [ bubblesDisabled ]);
|
||||
|
||||
const showNotification = (type: string, options: Map<string, string> = null) =>
|
||||
|
||||
Reference in New Issue
Block a user