feat: add builders club catalog ui flow

This commit is contained in:
Lorenzune
2026-04-07 14:40:51 +02:00
parent d271264b87
commit 954e477e47
26 changed files with 840 additions and 132 deletions
+23 -21
View File
@@ -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(() =>
{
+38 -5
View File
@@ -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>
+39 -6
View File
@@ -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>
@@ -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>
+8 -2
View File
@@ -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" />