mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
300 lines
18 KiB
TypeScript
300 lines
18 KiB
TypeScript
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
|
import { FC, useEffect, useState } from 'react';
|
|
import { FaBars, FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa';
|
|
import { CatalogType, GetConfigurationValue, LocalizeShortNumber, LocalizeText, SanitizeHtml } from '../../api';
|
|
import { Column, Grid, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
|
import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission, usePurse } 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 { CatalogBreadcrumbView } from './views/navigation/CatalogBreadcrumbView';
|
|
import { CatalogNavigationView } from './views/navigation/CatalogNavigationView';
|
|
import { CatalogSearchView } from './views/page/common/CatalogSearchView';
|
|
import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout';
|
|
import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView';
|
|
|
|
const CatalogClassicViewInner: FC<{}> = () =>
|
|
{
|
|
const { rootNode = null, currentPage = null, currentOffer = null, searchResult = null } = useCatalogData();
|
|
const { isVisible = false, setIsVisible = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], setSearchResult = null, currentType = CatalogType.NORMAL } = useCatalogUiState();
|
|
const { openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null } = useCatalogActions();
|
|
const catalogAdmin = useCatalogAdmin();
|
|
const adminMode = catalogAdmin?.adminMode ?? false;
|
|
const setAdminMode = catalogAdmin?.setAdminMode ?? (() =>
|
|
{});
|
|
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
|
|
const publishCatalog = catalogAdmin?.publishCatalog ?? (() =>
|
|
{});
|
|
const loading = catalogAdmin?.loading ?? false;
|
|
|
|
const isMod = useHasPermission('acc_catalogfurni');
|
|
const [ mobileMenuOpen, setMobileMenuOpen ] = useState(false);
|
|
const { purse = null } = usePurse();
|
|
const displayedCurrencies = GetConfigurationValue<number[]>('system.currency.types', []);
|
|
const activeCatalogNode = activeNodes?.[activeNodes.length - 1] ?? null;
|
|
// Strip SWF-style suffixes like "(BC)" or "(Hot)" but keep the
|
|
// pageId hint the gameserver appends when the viewer has
|
|
// ACC_CATALOG_IDS - that's a pure-numeric "(6)" trailer.
|
|
const getSwfTabLabel = (label: string) => (label || '').replace(/\s*\(\D[^)]*\)\s*$/g, '').trim();
|
|
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) =>
|
|
{
|
|
const parts = url.split('/');
|
|
|
|
if(parts.length < 2) return;
|
|
|
|
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':
|
|
if(parts.length > 2)
|
|
{
|
|
if(parts.length === 4)
|
|
{
|
|
switch(parts[2])
|
|
{
|
|
case 'offerId':
|
|
openPageByOfferId(parseInt(parts[3]));
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
openPageByName(parts[2]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
setIsVisible(true);
|
|
}
|
|
|
|
return;
|
|
}
|
|
},
|
|
eventUrlPrefix: 'catalog/'
|
|
};
|
|
|
|
AddLinkEventTracker(linkTracker);
|
|
|
|
return () => RemoveLinkEventTracker(linkTracker);
|
|
}, [ setIsVisible, openPageByOfferId, openPageByName, openCatalogByType, toggleCatalogByType ]);
|
|
|
|
return (
|
|
<>
|
|
{ isVisible &&
|
|
<NitroCardView classNames={ [ 'habbo-swf-window', 'habbo-swf-catalog-window', 'nitro-catalog-classic-window' ] } isResizable={ false } uniqueKey="catalog">
|
|
<NitroCardHeaderView className={ currentType === CatalogType.BUILDER ? 'builders-club-card-header' : '' } headerText={ LocalizeText('catalog.title') } onCloseClick={ () => setIsVisible(false) } style={ buildersClubHeaderStyle } />
|
|
<div className="nitro-catalog-classic-mobile-header">
|
|
{ isMod &&
|
|
<div className="nitro-catalog-classic-mobile-burger">
|
|
<button className="nitro-catalog-classic-burger-btn" onClick={ () => setMobileMenuOpen(value => !value) }>
|
|
<FaBars />
|
|
</button>
|
|
{ mobileMenuOpen &&
|
|
<div className="nitro-catalog-classic-burger-menu">
|
|
<button onClick={ () =>
|
|
{
|
|
setAdminMode(!adminMode); setMobileMenuOpen(false);
|
|
} }>
|
|
{ adminMode ? 'Exit Admin' : 'Admin' }
|
|
</button>
|
|
{ adminMode &&
|
|
<button disabled={ loading } onClick={ () =>
|
|
{
|
|
publishCatalog(); setMobileMenuOpen(false);
|
|
} }>
|
|
{ loading ? '...' : 'Publish' }
|
|
</button> }
|
|
</div> }
|
|
</div> }
|
|
<div className="nitro-catalog-classic-mobile-currency">
|
|
<div className="nitro-catalog-classic-coin">
|
|
<span>{ LocalizeShortNumber(purse?.credits ?? 0) }</span>
|
|
<LayoutCurrencyIcon type={ -1 } />
|
|
</div>
|
|
{ displayedCurrencies.map(type => (
|
|
<div key={ type } className="nitro-catalog-classic-coin">
|
|
<span>{ LocalizeShortNumber(purse?.activityPoints?.get(type) ?? 0) }</span>
|
|
<LayoutCurrencyIcon type={ type } />
|
|
</div>
|
|
)) }
|
|
</div>
|
|
</div>
|
|
{ adminMode &&
|
|
<button
|
|
className={ `nitro-catalog-classic-header-publish nitro-catalog-swf-button nitro-catalog-swf-buy-button ${ hasPendingChanges ? 'has-pending' : '' }` }
|
|
disabled={ loading }
|
|
onClick={ () => publishCatalog() }
|
|
title={ hasPendingChanges ? 'You have unsaved changes - click to publish' : 'Publish catalog' }
|
|
>
|
|
{ loading ? '...' : 'PUBLISH' }
|
|
</button> }
|
|
<NitroCardTabsView classNames={ [ 'nitro-catalog-classic-tabs-shell' ] } justifyContent="start">
|
|
{ rootNode && (rootNode.children.length > 0) && rootNode.children.map((child, index) =>
|
|
{
|
|
if(!adminMode && !child.isVisible) return null;
|
|
if(!adminMode && (index === 0) && getSwfTabLabel(child.localization).toLowerCase().includes('rari')) return null;
|
|
|
|
const isHidden = !child.isVisible;
|
|
|
|
return (
|
|
<NitroCardTabsItemView key={ `${ child.pageId }-${ child.pageName }-${ index }` } isActive={ child.isActive } onClick={ () =>
|
|
{
|
|
if(searchResult) setSearchResult(null);
|
|
|
|
activateNode(child);
|
|
} }>
|
|
<div className={ `flex items-center gap-1 ${ isHidden ? 'opacity-40' : '' }` }>
|
|
{ (child.iconId > 0) &&
|
|
<CatalogIconView icon={ child.iconId } className="nitro-catalog-classic-tab-icon" /> }
|
|
<span className="nitro-catalog-classic-tab-label truncate">{ getSwfTabLabel(child.localization) }</span>
|
|
{ adminMode && isHidden && <FaEyeSlash className="text-[8px] text-danger ml-1" /> }
|
|
{ adminMode &&
|
|
<div className="flex items-center gap-0.5 ml-1" onClick={ e => e.stopPropagation() }>
|
|
<FaEdit className="text-[8px] text-primary cursor-pointer hover:text-dark" title={ LocalizeText('catalog.admin.edit.title') }
|
|
onClick={ () =>
|
|
{
|
|
catalogAdmin.setEditingPageNode(child); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
|
} } />
|
|
<span className="cursor-pointer" title={ isHidden ? LocalizeText('catalog.admin.show') : LocalizeText('catalog.admin.hide') }
|
|
onClick={ () => catalogAdmin.togglePageVisible(child.pageId) }>
|
|
{ isHidden ? <FaEye className="text-[8px] text-success" /> : <FaEyeSlash className="text-[8px] text-muted" /> }
|
|
</span>
|
|
<FaTrash className="text-[8px] text-danger cursor-pointer hover:text-red-800" title={ LocalizeText('catalog.admin.delete.title') }
|
|
onClick={ () =>
|
|
{
|
|
if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) catalogAdmin.deletePage(child.pageId);
|
|
} } />
|
|
</div> }
|
|
</div>
|
|
</NitroCardTabsItemView>
|
|
);
|
|
}) }
|
|
{ isMod &&
|
|
<NitroCardTabsItemView classNames={ [ 'nitro-catalog-classic-admin-tab' ] } isActive={ adminMode } onClick={ () => setAdminMode(!adminMode) }>
|
|
<FaCog className={ `text-[10px] ${ adminMode ? 'animate-spin' : '' }` } style={ adminMode ? { animationDuration: '3s' } : {} } />
|
|
</NitroCardTabsItemView> }
|
|
</NitroCardTabsView>
|
|
<div className="nitro-catalog-classic-swf-header">
|
|
<div className="nitro-catalog-classic-swf-header-bg" style={ currentPage?.localization?.getImage(0) ? { backgroundImage: `url(${ currentPage.localization.getImage(0) })` } : undefined } />
|
|
<div className="nitro-catalog-classic-swf-header-icon">
|
|
<CatalogIconView icon={ activeCatalogNode?.iconId ?? rootNode?.iconId ?? 1 } />
|
|
</div>
|
|
<div className="nitro-catalog-classic-swf-header-copy">
|
|
<div className="nitro-catalog-classic-swf-header-title">
|
|
{ currentType === CatalogType.BUILDER ? LocalizeText('builder.header.title') : getSwfTabLabel(activeCatalogNode?.localization ?? LocalizeText('catalog.title')) }
|
|
</div>
|
|
{ currentType === CatalogType.BUILDER
|
|
? <div className="nitro-catalog-classic-swf-header-description">{ LocalizeText('builder.header.status.membership') }</div>
|
|
: <div className="nitro-catalog-classic-swf-header-description" dangerouslySetInnerHTML={ { __html: SanitizeHtml(currentPage?.localization?.getText(0) || '') } } /> }
|
|
</div>
|
|
</div>
|
|
<NitroCardContentView classNames={ [ 'nitro-catalog-classic-content-shell' ] }>
|
|
<CatalogBuildersClubStatusView />
|
|
{ adminMode && rootNode &&
|
|
<div className="flex items-center gap-2 mb-1 nitro-catalog-classic-admin-actions">
|
|
<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', captionSave: 'New Category', catalogMode: currentType, pageLayout: 'default_3x3', iconImage: 0, minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
|
>
|
|
<FaPlus className="text-[8px]" />
|
|
<span>{ LocalizeText('catalog.admin.new') }</span>
|
|
</button>
|
|
<button
|
|
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
|
|
onClick={ () =>
|
|
{
|
|
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true);
|
|
} }
|
|
>
|
|
<FaEdit className="text-[8px]" />
|
|
<span>{ LocalizeText('catalog.admin.root') }</span>
|
|
</button>
|
|
</div> }
|
|
<div className={ `nitro-catalog-classic-stage ${ navigationHidden ? 'is-navigation-hidden' : '' }` }>
|
|
{ !navigationHidden &&
|
|
<div className="nitro-catalog-classic-sidebar">
|
|
<div className="nitro-catalog-classic-search-shell">
|
|
<CatalogSearchView />
|
|
</div>
|
|
<div className="nitro-catalog-classic-navigation-shell">
|
|
{ activeNodes && (activeNodes.length > 0) &&
|
|
<CatalogNavigationView node={ activeNodes[0] } /> }
|
|
</div>
|
|
</div> }
|
|
<div className="nitro-catalog-classic-layout-shell">
|
|
<div className="nitro-catalog-classic-layout-header-shell">
|
|
<CatalogBreadcrumbView />
|
|
<div className="nitro-catalog-classic-layout-hero">
|
|
{ !!currentPage?.localization?.getImage(0) && <img src={ currentPage.localization.getImage(0) } /> }
|
|
</div>
|
|
</div>
|
|
<div className="nitro-catalog-classic-layout-container">
|
|
{ adminMode && <CatalogAdminPageEditView /> }
|
|
{ GetCatalogLayout(currentPage, () => setNavigationHidden(true)) }
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</NitroCardContentView>
|
|
</NitroCardView> }
|
|
<CatalogAdminOfferEditView />
|
|
<CatalogGiftView />
|
|
<MarketplacePostOfferView />
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const CatalogClassicView: FC<{}> = () =>
|
|
{
|
|
return (
|
|
<CatalogAdminProvider>
|
|
<CatalogClassicViewInner />
|
|
</CatalogAdminProvider>
|
|
);
|
|
};
|