Add emulator stats dashboard and refresh classic UI views

This commit is contained in:
Lorenzune
2026-05-25 10:10:40 +02:00
parent 4e1ceed53f
commit b038ca4542
38 changed files with 2476 additions and 336 deletions
+39 -28
View File
@@ -1,8 +1,8 @@
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 { CatalogType, GetConfigurationValue, LocalizeText } from '../../api';
import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
import { CatalogType, LocalizeText } from '../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
import { useCatalog } from '../../hooks';
import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext';
import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView';
@@ -10,7 +10,9 @@ 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';
@@ -23,7 +25,6 @@ const CatalogClassicViewInner: FC<{}> = () =>
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {});
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%)' }
@@ -113,21 +114,20 @@ const CatalogClassicViewInner: FC<{}> = () =>
return (
<>
{ isVisible &&
<NitroCardView className="w-[630px] h-[400px]" style={ GetConfigurationValue('catalog.headers') ? { width: 710 } : {} } uniqueKey="catalog">
<NitroCardView classNames={ [ '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 } />
{ /* 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)' } }>
<span> Admin Mode</span>
<div className="nitro-catalog-classic-admin-banner flex items-center justify-between text-[10px] font-bold px-3 py-0.5 uppercase tracking-wider">
<span>Admin Mode</span>
<button
className={ `px-3 py-0.5 rounded text-[10px] font-bold uppercase cursor-pointer transition-all ${ hasPendingChanges ? 'bg-success text-white animate-pulse shadow-md' : 'bg-white/50 text-dark hover:bg-success hover:text-white' }` }
disabled={ loading }
onClick={ () => publishCatalog() }
>
{ loading ? '...' : 'Publish' }
{ loading ? '...' : 'Publish' }
</button>
</div> }
<NitroCardTabsView>
<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;
@@ -140,10 +140,10 @@ const CatalogClassicViewInner: FC<{}> = () =>
if(searchResult) setSearchResult(null);
activateNode(child);
} } >
<div className={ `flex items-center gap-${ GetConfigurationValue('catalog.tab.icons') ? 1 : 0 } ${ isHidden ? 'opacity-40' : '' }` }>
{ GetConfigurationValue('catalog.tab.icons') && <CatalogIconView icon={ child.iconId } /> }
{ child.localization }
} }>
<div className={ `flex items-center gap-1 ${ isHidden ? 'opacity-40' : '' }` }>
<CatalogIconView icon={ child.iconId } />
<span className="truncate">{ 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() }>
@@ -160,17 +160,15 @@ const CatalogClassicViewInner: FC<{}> = () =>
</NitroCardTabsItemView>
);
}) }
{ /* Admin toggle button in tabs bar */ }
{ isMod &&
<NitroCardTabsItemView isActive={ adminMode } onClick={ () => setAdminMode(!adminMode) }>
<FaCog className={ `text-[10px] ${ adminMode ? 'animate-spin' : '' }` } style={ adminMode ? { animationDuration: '3s' } : {} } />
</NitroCardTabsItemView> }
</NitroCardTabsView>
<CatalogBuildersClubStatusView />
<NitroCardContentView>
{ /* Admin: add new root category */ }
<NitroCardContentView classNames={ [ 'nitro-catalog-classic-content-shell' ] }>
<CatalogBuildersClubStatusView />
{ adminMode && rootNode &&
<div className="flex items-center gap-2 mb-1">
<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', catalogMode: currentType, pageLayout: 'default_3x3', minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
@@ -186,17 +184,30 @@ const CatalogClassicViewInner: FC<{}> = () =>
<span>{ LocalizeText('catalog.admin.root') }</span>
</button>
</div> }
<Grid>
<div className={ `nitro-catalog-classic-stage ${ navigationHidden ? 'is-navigation-hidden' : '' }` }>
{ !navigationHidden &&
<Column overflow="auto" size={ 3 }>
{ activeNodes && (activeNodes.length > 0) &&
<CatalogNavigationView node={ activeNodes[0] } /> }
</Column> }
<Column overflow="hidden" size={ !navigationHidden ? 9 : 12 }>
{ adminMode && <CatalogAdminPageEditView /> }
{ GetCatalogLayout(currentPage, () => setNavigationHidden(true)) }
</Column>
</Grid>
<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 />
@@ -1,5 +1,4 @@
import { FC } from 'react';
import { FaChevronRight, FaHome } from 'react-icons/fa';
import { LocalizeText } from '../../../../api';
import { useCatalog } from '../../../../hooks';
@@ -10,25 +9,20 @@ export const CatalogBreadcrumbView: FC<{}> = () =>
if(!activeNodes || activeNodes.length === 0)
{
return (
<div className="flex items-center gap-1.5 text-xs text-catalog-text-muted">
<FaHome className="text-[10px]" />
<div className="nitro-catalog-classic-breadcrumb">
<span>{ LocalizeText('catalog.title') }</span>
</div>
);
}
return (
<div className="flex items-center gap-1 text-[11px] text-catalog-text-muted overflow-hidden min-w-0">
<FaHome
className="text-[10px] cursor-pointer hover:text-catalog-accent transition-colors shrink-0"
onClick={ () => activateNode(activeNodes[0]) }
/>
{ activeNodes.map((node, i) => (
<span key={ node.pageId } className="flex items-center gap-1 min-w-0">
<FaChevronRight className="text-[7px] opacity-30 shrink-0" />
<div className="nitro-catalog-classic-breadcrumb">
{ activeNodes.map((node, index) => (
<span key={ node.pageId } className="nitro-catalog-classic-breadcrumb-segment">
<span className="nitro-catalog-classic-breadcrumb-separator">&rsaquo;</span>
<span
className={ `truncate ${ i === activeNodes.length - 1 ? 'text-catalog-text font-semibold' : 'cursor-pointer hover:text-catalog-accent transition-colors' }` }
onClick={ i < activeNodes.length - 1 ? () => activateNode(node) : undefined }
className={ `truncate ${ index === activeNodes.length - 1 ? 'font-semibold' : 'cursor-pointer hover:underline' }` }
onClick={ index < activeNodes.length - 1 ? () => activateNode(node) : undefined }
>
{ node.localization }
</span>
@@ -73,10 +73,10 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
}, [ adminMode, node, catalogAdmin ]);
return (
<div className={ child ? 'pl-1.5 ml-1.5 border-l-2 border-card-grid-item-border' : '' }>
<div className={ `nitro-catalog-classic-navigation-node ${ child ? 'is-child' : '' }` }>
<div
ref={ dragRef }
className={ `group/nav flex items-center gap-1.5 px-1.5 py-[3px] mx-0.5 rounded cursor-pointer transition-all duration-100 text-[11px] ${ node.isActive ? 'bg-card-grid-item-active border border-card-grid-item-border-active shadow-inner1px font-bold' : 'border border-transparent hover:bg-card-grid-item-active' } ${ isDragOver ? 'ring-2 ring-primary ring-offset-1 bg-primary/10' : '' }` }
className={ `nitro-catalog-classic-navigation-item group/nav ${ node.isActive ? 'is-active' : '' } ${ node.isBranch ? 'is-branch' : 'is-leaf' } ${ node.isOpen ? 'is-open' : '' } ${ isDragOver ? 'is-drag-over' : '' }` }
draggable={ adminMode }
onClick={ () => activateNode(node) }
onDragLeave={ adminMode ? handleDragLeave : undefined }
@@ -85,13 +85,13 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
onDrop={ adminMode ? handleDrop : undefined }
>
{ adminMode &&
<FaArrowsAlt className="text-[7px] text-muted cursor-grab shrink-0 opacity-0 group-hover/nav:opacity-60" /> }
<div className="w-5 h-5 flex items-center justify-center shrink-0">
<FaArrowsAlt className="nitro-catalog-classic-navigation-drag text-[7px] text-muted cursor-grab shrink-0 opacity-0 group-hover/nav:opacity-60" /> }
<div className="nitro-catalog-classic-navigation-icon">
<CatalogIconView icon={ node.iconId } />
</div>
<span className="flex-1 truncate" title={ adminMode ? `Page ID: ${ node.pageId }` : undefined }>{ node.localization }</span>
<span className="nitro-catalog-classic-navigation-label" title={ adminMode ? `Page ID: ${ node.pageId }` : undefined }>{ node.localization }</span>
{ adminMode &&
<div className="flex items-center gap-1 opacity-0 group-hover/nav:opacity-100 transition-opacity">
<div className="nitro-catalog-classic-navigation-admin flex items-center gap-1 opacity-0 group-hover/nav:opacity-100 transition-opacity">
<FaPlus
className="text-[8px] text-success hover:text-green-800"
title={ LocalizeText('catalog.admin.create.subpage') }
@@ -125,11 +125,11 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
</div> }
{ !adminMode && node.pageId > 0 &&
<FaStar
className={ `text-[8px] transition-all duration-100 cursor-pointer shrink-0 ${ isFav ? 'text-warning opacity-100' : 'text-muted opacity-0 group-hover/nav:opacity-100 hover:text-warning' }` }
className={ `nitro-catalog-classic-navigation-favorite text-[8px] transition-all duration-100 cursor-pointer shrink-0 ${ isFav ? 'text-warning opacity-100' : 'text-muted opacity-0 group-hover/nav:opacity-100 hover:text-warning' }` }
onClick={ e => { e.stopPropagation(); toggleFavoritePage(node.pageId); } }
/> }
{ node.isBranch &&
<span className="text-[9px] text-muted shrink-0">
<span className="nitro-catalog-classic-navigation-caret text-[9px] text-muted shrink-0">
{ node.isOpen ? <FaCaretUp /> : <FaCaretDown /> }
</span> }
</div>
@@ -15,7 +15,7 @@ export const CatalogNavigationView: FC<CatalogNavigationViewProps> = props =>
const { searchResult = null } = useCatalog();
return (
<div className="flex flex-col gap-px px-0.5 py-0.5">
<div className="nitro-catalog-classic-navigation-list">
{ searchResult && (searchResult.filteredNodes.length > 0) && searchResult.filteredNodes.map((n, index) =>
{
return <CatalogNavigationItemView key={ index } node={ n } />;
@@ -61,10 +61,9 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
return (
<LayoutGridItem
className="group/tile relative"
className={ `group/tile relative ${ itemActive ? 'is-active' : '' }` }
itemActive={ itemActive }
itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) }
itemImage={ iconUrl }
itemUniqueNumber={ product.uniqueLimitedItemSeriesSize }
itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) }
title={ `ID: ${ product.productClassId } | Offer: ${ offer.offerId }` }
@@ -73,6 +72,8 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
onMouseUp={ onMouseEvent }
{ ...rest }
>
{ iconUrl && !(offer.product.productType === ProductTypeEnum.ROBOT) &&
<div className="nitro-catalog-classic-grid-offer-icon" style={ { backgroundImage: `url(${ iconUrl })` } } /> }
{ (offer.product.productType === ProductTypeEnum.ROBOT) &&
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> }
<div
@@ -22,10 +22,10 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
const adminMode = catalogAdmin?.adminMode ?? false;
return (
<div className="flex flex-col h-full gap-2">
<div className="nitro-catalog-classic-default-layout flex flex-col h-full gap-2">
{ /* Admin: quick actions */ }
{ adminMode && !catalogAdmin.editingPageData &&
<div className="flex gap-2">
<div className="flex gap-2 nitro-catalog-classic-default-admin">
<button
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } }
@@ -42,9 +42,9 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
{ /* Product detail card */ }
{ currentOffer &&
<div className="flex gap-0 bg-white rounded border-2 border-card-grid-item-border overflow-hidden">
<div className="nitro-catalog-classic-offer-panel flex gap-0 overflow-hidden">
{ /* Preview area */ }
<div className="w-[140px] min-w-[140px] bg-card-grid-item relative flex items-center justify-center border-r-2 border-card-grid-item-border">
<div className="nitro-catalog-classic-offer-preview relative flex items-center justify-center">
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
@@ -54,7 +54,7 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
<CatalogAddOnBadgeWidgetView className="scale-2" /> }
</div>
{ /* Product info + purchase */ }
<div className="flex flex-col flex-1 min-w-0 p-2.5 gap-2">
<div className="nitro-catalog-classic-offer-info flex flex-col flex-1 min-w-0 gap-2">
{ /* Title row */ }
<div>
<div className="flex items-start justify-between gap-2">
@@ -87,17 +87,17 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
{ /* Welcome/description card */ }
{ !currentOffer &&
<div className="flex items-center gap-3 p-2.5 bg-white rounded border-2 border-card-grid-item-border">
<div className="nitro-catalog-classic-welcome flex items-center gap-3">
{ !!page.localization.getImage(1) &&
<img className="w-[70px] h-[70px] object-contain rounded shrink-0" src={ page.localization.getImage(1) } /> }
<Text className="text-[11px]! text-muted" dangerouslySetInnerHTML={ { __html: SanitizeHtml(page.localization.getText(0)) } } />
</div> }
{ /* Item grid */ }
<div className="flex-1 overflow-auto min-h-0">
<div className="nitro-catalog-classic-grid-shell flex-1 overflow-auto min-h-0">
{ GetConfigurationValue('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
<CatalogItemGridWidgetView columnCount={ 7 } columnMinHeight={ 50 } columnMinWidth={ 50 } />
<CatalogItemGridWidgetView className="nitro-catalog-classic-grid" columnCount={ 7 } columnMinHeight={ 50 } columnMinWidth={ 50 } />
</div>
</div>
);