diff --git a/src/api/emustats/EmuStatsApi.ts b/src/api/emustats/EmuStatsApi.ts new file mode 100644 index 0000000..95606c7 --- /dev/null +++ b/src/api/emustats/EmuStatsApi.ts @@ -0,0 +1,198 @@ +import { GetConfiguration } from '@nitrots/nitro-renderer'; +import { getAccessToken } from '../auth'; + +export interface EmuStatsOverview +{ + uptimeSeconds: number; + lastRefreshEpochMs: number; + guiStatus: string; + memoryUsedMb: number; + memoryMaxMb: number; + memoryAllocatedMb: number; + memoryUsagePercent: number; + cpuLoadPercent: number; + activeOsThreads: number; + connectedPlayers: number; + loadedRooms: number; + wiredTickables: number; + peakPlayers: number; + activeWebSocketSessions: number; + peakWebSocketSessions: number; + averageRoomCycleMs: number; + worstRoomCycleMs: number; + worstRoomCycleRoomId: number; + worstRoomCycleRoomName: string; + delayedEventsPending: number; + overloadedWiredRooms: number; + heavyWiredRooms: number; + wiredActivityPerSecond: number; +} + +export interface EmuStatsMemoryPoint +{ + timestamp: number; + usedMb: number; + maxMb: number; + usagePercent: number; +} + +export interface EmuStatsUserRow +{ + id: number; + username: string; + rank: string; + credits: number; + roomId: number; +} + +export interface EmuStatsRoomRow +{ + roomId: number; + name: string; + players: number; + items: number; + tickables: number; + cpuMs: number; + estimatedRamKb: number; + thread: string; +} + +export interface EmuStatsWiredRow +{ + roomId: number; + averageTickMs: number; + peakTickMs: number; + usagePercent: number; + delayedEventsPending: number; + overloaded: boolean; + heavy: boolean; +} + +export interface EmuStatsWiredTopRoomRow +{ + roomId: number; + name: string; + usagePercent: number; + averageTickMs: number; + peakTickMs: number; + delayedEventsPending: number; + activityPerSecond: number; + heavy: boolean; +} + +export interface EmuStatsDatabasePool +{ + activeConnections: number; + idleConnections: number; + totalConnections: number; + waitingThreads: number; + maxConnections: number; +} + +export interface EmuStatsScheduler +{ + queuedTasks: number; + activeThreads: number; + poolSize: number; + completedTasks: number; + running: boolean; +} + +export interface EmuStatsNetwork +{ + incomingPacketsPerSecond: number; + outgoingPacketsPerSecond: number; + incomingKilobytesPerSecond: number; + outgoingKilobytesPerSecond: number; + totalIncomingPackets: number; + totalOutgoingPackets: number; +} + +export interface EmuStatsGarbageCollector +{ + totalCollections: number; + totalCollectionTimeMs: number; + collectionsSinceLastSample: number; + lastObservedPauseMs: number; + sampledAtEpochMs: number; +} + +export interface EmuStatsSnapshot +{ + overview: EmuStatsOverview; + memoryHistory: EmuStatsMemoryPoint[]; + users: EmuStatsUserRow[]; + rooms: EmuStatsRoomRow[]; + wired: EmuStatsWiredRow[]; + wiredTopRooms: EmuStatsWiredTopRoomRow[]; + databasePool: EmuStatsDatabasePool; + scheduler: EmuStatsScheduler; + network: EmuStatsNetwork; + garbageCollector: EmuStatsGarbageCollector; +} + +const interpolate = (value: string): string => +{ + try { return GetConfiguration().interpolate(value); } + catch { return value; } +}; + +const getUrl = (): string => +{ + const configured = GetConfiguration().getValue('emustats.endpoint', '${api.url}/api/emustats'); + + return interpolate(configured); +}; + +const buildHeaders = (): Record => +{ + const headers: Record = { + Accept: 'application/json', + 'X-Requested-With': 'NitroEmuStats' + }; + + const token = getAccessToken(); + + if(token) headers.Authorization = `Bearer ${ token }`; + + return headers; +}; + +let cacheValue: EmuStatsSnapshot = null; + +const parseJson = async (response: Response): Promise => +{ + const text = await response.text(); + + if(!text) return {} as T; + + try { return JSON.parse(text) as T; } + catch { throw new Error('Invalid emulator stats response.'); } +}; + +export const fetchEmuStats = async (force = false): Promise => +{ + if(!force && cacheValue) return cacheValue; + + const response = await fetch(getUrl(), { + method: 'GET', + credentials: 'include', + headers: buildHeaders() + }); + + const payload = await parseJson(response); + + if(!response.ok) + { + throw new Error(payload?.error || `Request failed (${ response.status }).`); + } + + cacheValue = payload; + + return payload; +}; + +export const getCachedEmuStats = (): EmuStatsSnapshot => +{ + return cacheValue; +}; diff --git a/src/api/emustats/index.ts b/src/api/emustats/index.ts new file mode 100644 index 0000000..bbf5c89 --- /dev/null +++ b/src/api/emustats/index.ts @@ -0,0 +1 @@ +export * from './EmuStatsApi'; diff --git a/src/api/index.ts b/src/api/index.ts index 321608a..47caa6e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,6 +8,7 @@ export * from './camera'; export * from './campaign'; export * from './catalog'; export * from './chat-history'; +export * from './emustats'; export * from './events'; export * from './friends'; export * from './groups'; diff --git a/src/assets/images/catalog/classic/catalogue-hover.png b/src/assets/images/catalog/classic/catalogue-hover.png new file mode 100644 index 0000000..05fc7b3 Binary files /dev/null and b/src/assets/images/catalog/classic/catalogue-hover.png differ diff --git a/src/assets/images/catalog/classic/catalogue-normal.png b/src/assets/images/catalog/classic/catalogue-normal.png new file mode 100644 index 0000000..17f0b04 Binary files /dev/null and b/src/assets/images/catalog/classic/catalogue-normal.png differ diff --git a/src/assets/images/catalog/classic/category-collapse.png b/src/assets/images/catalog/classic/category-collapse.png new file mode 100644 index 0000000..a4f5b81 Binary files /dev/null and b/src/assets/images/catalog/classic/category-collapse.png differ diff --git a/src/assets/images/catalog/classic/category-expand.png b/src/assets/images/catalog/classic/category-expand.png new file mode 100644 index 0000000..5ee943e Binary files /dev/null and b/src/assets/images/catalog/classic/category-expand.png differ diff --git a/src/assets/images/catalog/classic/scroll-down.png b/src/assets/images/catalog/classic/scroll-down.png new file mode 100644 index 0000000..833a3bc Binary files /dev/null and b/src/assets/images/catalog/classic/scroll-down.png differ diff --git a/src/assets/images/catalog/classic/scroll-thumb.png b/src/assets/images/catalog/classic/scroll-thumb.png new file mode 100644 index 0000000..d881aa4 Binary files /dev/null and b/src/assets/images/catalog/classic/scroll-thumb.png differ diff --git a/src/assets/images/catalog/classic/scroll-up.png b/src/assets/images/catalog/classic/scroll-up.png new file mode 100644 index 0000000..34688d0 Binary files /dev/null and b/src/assets/images/catalog/classic/scroll-up.png differ diff --git a/src/assets/images/catalog/classic/tab-bg-hilite.png b/src/assets/images/catalog/classic/tab-bg-hilite.png new file mode 100644 index 0000000..b07e8d8 Binary files /dev/null and b/src/assets/images/catalog/classic/tab-bg-hilite.png differ diff --git a/src/assets/images/catalog/classic/tab-bg-sel.png b/src/assets/images/catalog/classic/tab-bg-sel.png new file mode 100644 index 0000000..e351bce Binary files /dev/null and b/src/assets/images/catalog/classic/tab-bg-sel.png differ diff --git a/src/assets/images/catalog/classic/tab-bg-unsel.png b/src/assets/images/catalog/classic/tab-bg-unsel.png new file mode 100644 index 0000000..9acc5b9 Binary files /dev/null and b/src/assets/images/catalog/classic/tab-bg-unsel.png differ diff --git a/src/assets/images/user-profile/block.png b/src/assets/images/user-profile/block.png new file mode 100644 index 0000000..f1df860 Binary files /dev/null and b/src/assets/images/user-profile/block.png differ diff --git a/src/assets/images/user-profile/index.ts b/src/assets/images/user-profile/index.ts new file mode 100644 index 0000000..97d0224 --- /dev/null +++ b/src/assets/images/user-profile/index.ts @@ -0,0 +1,5 @@ +export { default as block } from './block.png'; +export { default as level } from './level.png'; +export { default as me } from './me.png'; +export { default as profile } from './profile.png'; +export { default as rooms } from './rooms.png'; diff --git a/src/assets/images/user-profile/level.png b/src/assets/images/user-profile/level.png new file mode 100644 index 0000000..924077b Binary files /dev/null and b/src/assets/images/user-profile/level.png differ diff --git a/src/assets/images/user-profile/me.png b/src/assets/images/user-profile/me.png new file mode 100644 index 0000000..8454dd8 Binary files /dev/null and b/src/assets/images/user-profile/me.png differ diff --git a/src/assets/images/user-profile/profile.png b/src/assets/images/user-profile/profile.png new file mode 100644 index 0000000..027d481 Binary files /dev/null and b/src/assets/images/user-profile/profile.png differ diff --git a/src/assets/images/user-profile/rooms.png b/src/assets/images/user-profile/rooms.png new file mode 100644 index 0000000..43c4d8e Binary files /dev/null and b/src/assets/images/user-profile/rooms.png differ diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 1d2c5d0..5f6882f 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -12,6 +12,7 @@ import { CampaignView } from './campaign/CampaignView'; import { CatalogView } from './catalog/CatalogView'; import { ChatHistoryView } from './chat-history/ChatHistoryView'; import { CustomizeNickIconView } from './customize/CustomizeNickIconView'; +import { EmuStatsView } from './emustats/EmuStatsView'; import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView'; import { FurniEditorView } from './furni-editor/FurniEditorView'; import { FriendsView } from './friends/FriendsView'; @@ -149,6 +150,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/catalog/CatalogClassicView.tsx b/src/components/catalog/CatalogClassicView.tsx index 0d0e2e6..a3f2a73 100644 --- a/src/components/catalog/CatalogClassicView.tsx +++ b/src/components/catalog/CatalogClassicView.tsx @@ -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'; @@ -117,21 +119,20 @@ const CatalogClassicViewInner: FC<{}> = () => return ( <> { isVisible && - + setIsVisible(false) } style={ buildersClubHeaderStyle } /> - { /* Admin banner */ } { adminMode && -
- ⚙ Admin Mode +
+ Admin Mode
} - + { rootNode && (rootNode.children.length > 0) && rootNode.children.map((child, index) => { if(!adminMode && !child.isVisible) return null; @@ -144,10 +145,10 @@ const CatalogClassicViewInner: FC<{}> = () => if(searchResult) setSearchResult(null); activateNode(child); - } } > -
- { GetConfigurationValue('catalog.tab.icons') && } - { child.localization } + } }> +
+ + { child.localization } { adminMode && isHidden && } { adminMode &&
e.stopPropagation() }> @@ -170,17 +171,15 @@ const CatalogClassicViewInner: FC<{}> = () => ); }) } - { /* Admin toggle button in tabs bar */ } { isMod && setAdminMode(!adminMode) }> } - - - { /* Admin: add new root category */ } + + { adminMode && rootNode && -
+
} - +
{ !navigationHidden && - - { activeNodes && (activeNodes.length > 0) && - } - } - - { adminMode && } - { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) } - - +
+
+ +
+
+ { activeNodes && (activeNodes.length > 0) && + } +
+
} +
+
+ +
+ { !!currentPage?.localization?.getImage(0) && } +
+
+
+ { adminMode && } + { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) } +
+
+
} diff --git a/src/components/catalog/views/navigation/CatalogBreadcrumbView.tsx b/src/components/catalog/views/navigation/CatalogBreadcrumbView.tsx index f677606..fc9cb58 100644 --- a/src/components/catalog/views/navigation/CatalogBreadcrumbView.tsx +++ b/src/components/catalog/views/navigation/CatalogBreadcrumbView.tsx @@ -1,5 +1,4 @@ import { FC } from 'react'; -import { FaChevronRight, FaHome } from 'react-icons/fa'; import { LocalizeText } from '../../../../api'; import { useCatalogActions, useCatalogUiState } from '../../../../hooks'; @@ -11,25 +10,20 @@ export const CatalogBreadcrumbView: FC<{}> = () => if(!activeNodes || activeNodes.length === 0) { return ( -
- +
{ LocalizeText('catalog.title') }
); } return ( -
- activateNode(activeNodes[0]) } - /> - { activeNodes.map((node, i) => ( - - +
+ { activeNodes.map((node, index) => ( + + 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 } diff --git a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx index 32cee1c..973179c 100644 --- a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx +++ b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx @@ -74,10 +74,10 @@ export const CatalogNavigationItemView: FC = pro }, [ adminMode, node, catalogAdmin ]); return ( -
+
activateNode(node) } onDragLeave={ adminMode ? handleDragLeave : undefined } @@ -86,13 +86,13 @@ export const CatalogNavigationItemView: FC = pro onDrop={ adminMode ? handleDrop : undefined } > { adminMode && - } -
+ } +
- { node.localization } + { node.localization } { adminMode && -
+
= pro } } /> } { node.isBranch && - + { node.isOpen ? : } }
diff --git a/src/components/catalog/views/navigation/CatalogNavigationView.tsx b/src/components/catalog/views/navigation/CatalogNavigationView.tsx index 97907f6..d40886e 100644 --- a/src/components/catalog/views/navigation/CatalogNavigationView.tsx +++ b/src/components/catalog/views/navigation/CatalogNavigationView.tsx @@ -15,7 +15,7 @@ export const CatalogNavigationView: FC = props => const { searchResult = null } = useCatalogData(); return ( -
+
{ searchResult && (searchResult.filteredNodes.length > 0) && searchResult.filteredNodes.map((n, index) => { return ; diff --git a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx index 660f1ef..936b8dc 100644 --- a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx +++ b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx @@ -62,10 +62,9 @@ export const CatalogGridOfferView: FC = props => return ( = props => onMouseUp={ onMouseEvent } { ...rest } > + { iconUrl && !(offer.product.productType === ProductTypeEnum.ROBOT) && +
} { (offer.product.productType === ProductTypeEnum.ROBOT) && }
= props => const adminMode = catalogAdmin?.adminMode ?? false; return ( -
+
{ /* Admin: quick actions */ } { adminMode && !catalogAdmin.editingPageData && -
+
+ )) } + +
+ +

Auto refresh every { REFRESH_INTERVAL_MS / 1000 }s

+
+ +
+
+
+

{ navItems.find(item => item.key === section)?.label || 'Overview' }

+

Live operational view of emulator health, activity and wired performance.

+
+ { overview && +
+ { overview.guiStatus } +
} +
+ { error && +
+ { error } +
} + { isLoading && !snapshot && +
+ Loading emulator stats... +
} + { !isLoading && !snapshot && !error && +
+ No emulator stats available yet. +
} + { snapshot && +
+ { content } +
} +
+
+ + + ); +}; diff --git a/src/components/groups/views/GroupInformationView.tsx b/src/components/groups/views/GroupInformationView.tsx index 5da2719..5536a84 100644 --- a/src/components/groups/views/GroupInformationView.tsx +++ b/src/components/groups/views/GroupInformationView.tsx @@ -1,21 +1,20 @@ import { CreateLinkEvent, GetSessionDataManager, GroupInformationParser, GroupRemoveMemberComposer } from '@nitrots/nitro-renderer'; import { FC } from 'react'; import { CatalogPageName, GetGroupManager, GetGroupMembers, GroupMembershipType, GroupType, LocalizeText, SendMessageComposer, TryJoinGroup, TryVisitRoom } from '../../../api'; -import { Button, Column, Grid, GridProps, LayoutBadgeImageView, Text } from '../../../common'; +import { Button, LayoutBadgeImageView, Text } from '../../../common'; import { useNotification } from '../../../hooks'; const STATES: string[] = [ 'regular', 'exclusive', 'private' ]; -interface GroupInformationViewProps extends GridProps +interface GroupInformationViewProps { groupInformation: GroupInformationParser; - onJoin?: () => void; onClose?: () => void; } export const GroupInformationView: FC = props => { - const { groupInformation = null, onClose = null, overflow = 'hidden', ...rest } = props; + const { groupInformation = null, onClose = null } = props; const { showConfirm = null } = useNotification(); const isRealOwner = (groupInformation && (groupInformation.ownerName === GetSessionDataManager().userName)); @@ -103,49 +102,47 @@ export const GroupInformationView: FC = props => if(!groupInformation) return null; return ( - - -
- +
+
+
+
- - handleAction('members') }>{ LocalizeText('group.membercount', [ 'totalMembers' ], [ groupInformation.membersCount.toString() ]) } +
+ handleAction('members') }>{ LocalizeText('group.membercount', [ 'totalMembers' ], [ groupInformation.membersCount.toString() ]) } { (groupInformation.pendingRequestsCount > 0) && handleAction('members_pending') }>{ LocalizeText('group.pendingmembercount', [ 'amount' ], [ groupInformation.pendingRequestsCount.toString() ]) } } - { groupInformation.isOwner && - handleAction('manage') }>{ LocalizeText('group.manage') } } - - { getRoleIcon() } - -
-
-
-
- { groupInformation.title } -
- - { groupInformation.canMembersDecorate && - } -
-
- { LocalizeText('group.created', [ 'date', 'owner' ], [ groupInformation.createdAt, groupInformation.ownerName ]) } -
- { groupInformation.description }
-
-
- handleAction('homeroom') }>{ LocalizeText('group.linktobase') } - handleAction('furniture') }>{ LocalizeText('group.buyfurni') } - handleAction('popular_groups') }>{ LocalizeText('group.showgroups') } - { groupInformation.hasForum && - handleAction('forum') }>{ LocalizeText('group.showforum') } } -
- { (groupInformation.type !== GroupType.PRIVATE || groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType === GroupMembershipType.MEMBER) && - } +
+ { getRoleIcon() }
- +
+
+
+ { groupInformation.title } +
+ + { groupInformation.canMembersDecorate && + } +
+
+ { LocalizeText('group.created', [ 'date', 'owner' ], [ groupInformation.createdAt, groupInformation.ownerName ]) } +
+ { groupInformation.description } +
+ handleAction('homeroom') }>{ LocalizeText('group.linktobase') } + handleAction('furniture') }>{ LocalizeText('group.buyfurni') } + handleAction('popular_groups') }>{ LocalizeText('group.showgroups') } + { groupInformation.hasForum && + handleAction('forum') }>{ LocalizeText('group.showforum') } } + { groupInformation.isOwner && + handleAction('manage') }>{ LocalizeText('group.manage') } } +
+ { (groupInformation.type !== GroupType.PRIVATE || groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType === GroupMembershipType.MEMBER) && + } +
+
); }; diff --git a/src/components/user-profile/GroupsContainerView.tsx b/src/components/user-profile/GroupsContainerView.tsx index 3358f60..b4e75a9 100644 --- a/src/components/user-profile/GroupsContainerView.tsx +++ b/src/components/user-profile/GroupsContainerView.tsx @@ -1,7 +1,7 @@ import { GroupInformationComposer, GroupInformationEvent, GroupInformationParser, HabboGroupEntryData } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer, ToggleFavoriteGroup } from '../../api'; -import { AutoGrid, Column, Grid, GridProps, LayoutBadgeImageView, LayoutGridItem } from '../../common'; +import { LocalizeText, SendMessageComposer, ToggleFavoriteGroup } from '../../api'; +import { Column, GridProps, LayoutBadgeImageView, LayoutGridItem } from '../../common'; import { useMessageEvent } from '../../hooks'; import { GroupInformationView } from '../groups/views/GroupInformationView'; @@ -14,7 +14,7 @@ interface GroupsContainerViewProps extends GridProps export const GroupsContainerView: FC = props => { - const { itsMe = null, groups = null, onLeaveGroup = null, overflow = 'hidden', gap = 2, ...rest } = props; + const { itsMe = null, groups = null, onLeaveGroup = null } = props; const [ selectedGroupId, setSelectedGroupId ] = useState(null); const [ groupInformation, setGroupInformation ] = useState(null); @@ -55,7 +55,7 @@ export const GroupsContainerView: FC = props => if(!groups || !groups.length) { return ( - +
@@ -66,25 +66,28 @@ export const GroupsContainerView: FC = props => } return ( - - - +
+
+
+ { LocalizeText('extendedprofile.groups.count', [ 'count' ], [ groups.length.toString() ]) } +
+
{ groups.map((group, index) => { return ( - setSelectedGroupId(group.groupId) }> + setSelectedGroupId(group.groupId) }> { itsMe && - ToggleFavoriteGroup(group) } /> } + { event.stopPropagation(); ToggleFavoriteGroup(group); } } /> } ); }) } - - - +
+
+
{ groupInformation && } - - +
+
); }; diff --git a/src/components/user-profile/RelationshipsContainerView.tsx b/src/components/user-profile/RelationshipsContainerView.tsx index 52ef6a8..4de625a 100644 --- a/src/components/user-profile/RelationshipsContainerView.tsx +++ b/src/components/user-profile/RelationshipsContainerView.tsx @@ -23,24 +23,24 @@ export const RelationshipsContainerView: FC = p const relationshipName = RelationshipStatusEnum.RELATIONSHIP_NAMES[type].toLocaleLowerCase(); return ( -
- +
+ -
-
-

(relationshipInfo && (relationshipInfo.randomFriendId >= 1) && GetUserProfile(relationshipInfo.randomFriendId)) }> +

+
+

(relationshipInfo && (relationshipInfo.randomFriendId >= 1) && GetUserProfile(relationshipInfo.randomFriendId)) }> { (!relationshipInfo || (relationshipInfo.friendCount === 0)) && LocalizeText('extendedprofile.add.friends') } { (relationshipInfo && (relationshipInfo.friendCount >= 1)) && relationshipInfo.randomFriendName }

{ (relationshipInfo && (relationshipInfo.friendCount >= 1)) && -
- +
+
}
-

+

{ (!relationshipInfo || (relationshipInfo.friendCount === 0)) && LocalizeText('extendedprofile.no.friends.in.this.category') } { (relationshipInfo && (relationshipInfo.friendCount > 1)) && diff --git a/src/components/user-profile/UserContainerView.tsx b/src/components/user-profile/UserContainerView.tsx index 029c91f..73b3ca2 100644 --- a/src/components/user-profile/UserContainerView.tsx +++ b/src/components/user-profile/UserContainerView.tsx @@ -1,28 +1,35 @@ -import { GetSessionDataManager, RequestFriendComposer, UserProfileParser } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { CreateLinkEvent, GetSessionDataManager, RelationshipStatusInfoMessageParser, RequestFriendComposer, UserProfileParser } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; import { FriendlyTime, LocalizeText, SendMessageComposer } from '../../api'; -import { LayoutAvatarImageView, Text, UserIdentityView } from '../../common'; +import { LayoutAvatarImageView, LayoutBadgeImageView, Text, UserIdentityView } from '../../common'; +import { badgeEmblemDefault } from '../../assets/images/leaderboard_badge'; +import { level as profileLevelIcon, rooms as profileRoomsIcon } from '../../assets/images/user-profile'; +import { RelationshipsContainerView } from './RelationshipsContainerView'; -export const UserContainerView: FC<{ - userProfile: UserProfileParser; -}> = props => +interface UserContainerViewProps { - const { userProfile = null } = props; + userProfile: UserProfileParser; + userBadges?: string[]; + userRelationships?: RelationshipStatusInfoMessageParser; + onOpenRooms?: () => void; +} + +export const UserContainerView: FC = props => +{ + const { userProfile = null, userBadges = [], userRelationships = null, onOpenRooms = null } = props; const [ requestSent, setRequestSent ] = useState(userProfile.requestSent); - const isOwnProfile = (userProfile.id === GetSessionDataManager().userId); - const canSendFriendRequest = !requestSent && (!isOwnProfile && !userProfile.isMyFriend && !userProfile.requestSent); + const infostandBackgroundClass = `background-${ userProfile.backgroundId ?? 'default' }`; + const infostandStandClass = `stand-${ userProfile.standId ?? 'default' }`; + const infostandOverlayClass = `overlay-${ userProfile.overlayId ?? 'default' }`; + const selectedBadges = useMemo(() => [ ...userBadges ].slice(0, 5), [ userBadges ]); + const totalBadges = ((userProfile as any).totalBadges ?? userBadges.length ?? 0); - const infostandBackgroundClass = `background-${userProfile.backgroundId ?? 'default'}`; - const infostandStandClass = `stand-${userProfile.standId ?? 'default'}`; - const infostandOverlayClass = `overlay-${userProfile.overlayId ?? 'default'}`; - const profileCardBgClass = userProfile.cardBackgroundId ? `card-background-${userProfile.cardBackgroundId}` : ''; const addFriend = () => { setRequestSent(true); - SendMessageComposer(new RequestFriendComposer(userProfile.username)); }; @@ -32,51 +39,87 @@ export const UserContainerView: FC<{ }, [ userProfile ]); return ( -

-
-
- -
-
+
+
+
+
+
+
+ +
+
+
+ +

{ userProfile.motto || '\u00A0' }

+

+

+

+ { LocalizeText('extendedprofile.achievementscore') } { userProfile.achievementPoints } +

+
+
+ +
+
+ { canSendFriendRequest && + } + { !canSendFriendRequest && + <> + + + { isOwnProfile && LocalizeText('extendedprofile.me') } + { userProfile.isMyFriend && LocalizeText('extendedprofile.friend') } + { (requestSent || userProfile.requestSent) && LocalizeText('extendedprofile.friendrequestsent') } + + } +
+
+
+
-
-
- -

{ userProfile.motto }

+ { isOwnProfile && +
+ + +
} + +
+ { [ 0, 1, 2, 3, 4 ].map(index => ( + + )) } +
-
-

- -

+

+

- -

- { LocalizeText('extendedprofile.achievementscore') } { userProfile.achievementPoints } -

+

{ LocalizeText('extendedprofile.relstatus') }

+ { userRelationships && + } + { !userRelationships && + { LocalizeText('generic.loading') } }
+
-
- { userProfile.isOnline && - } - - { !userProfile.isOnline && - } - -
- { canSendFriendRequest && - - { LocalizeText('extendedprofile.addasafriend') } - } - - { !canSendFriendRequest && - <> - - - { isOwnProfile && -

{ LocalizeText('extendedprofile.me') }

} - - { userProfile.isMyFriend && -

{ LocalizeText('extendedprofile.friend') }

} - - { (requestSent || userProfile.requestSent) && -

{ LocalizeText('extendedprofile.friendrequestsent') }

} - } -
+
+ + +
+ + { LocalizeText('extendedprofile.achievementscore') } + { userProfile.achievementPoints }
diff --git a/src/components/user-profile/UserProfileView.tsx b/src/components/user-profile/UserProfileView.tsx index a4d39c3..fd5a3b5 100644 --- a/src/components/user-profile/UserProfileView.tsx +++ b/src/components/user-profile/UserProfileView.tsx @@ -1,31 +1,22 @@ -import { ExtendedProfileChangedMessageEvent, GetSessionDataManager, NavigatorSearchComposer, NavigatorSearchEvent, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomDataParser, RoomEngineObjectEvent, RoomObjectCategory, RoomObjectType, UserCurrentBadgesComposer, UserCurrentBadgesEvent, UserProfileEvent, UserProfileParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; +import { ExtendedProfileChangedMessageEvent, GetSessionDataManager, NavigatorSearchComposer, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomEngineObjectEvent, RoomObjectCategory, RoomObjectType, UserCurrentBadgesComposer, UserCurrentBadgesEvent, UserProfileEvent, UserProfileParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; import { FC, useState } from 'react'; -import { CreateRoomSession, GetRoomSession, GetUserProfile, LocalizeText, SendMessageComposer } from '../../api'; -import { Flex, Text } from '../../common'; -import { BadgeInfoView } from './BadgeInfoView'; +import { GetRoomSession, GetUserProfile, LocalizeText, SendMessageComposer } from '../../api'; import { useMessageEvent, useNitroEvent } from '../../hooks'; import { NitroCard } from '../../layout'; -import { FriendsContainerView } from './FriendsContainerView'; import { GroupsContainerView } from './GroupsContainerView'; import { UserContainerView } from './UserContainerView'; -type ProfileTab = 'badge' | 'friends' | 'rooms' | 'groups'; - -export const UserProfileView: FC<{}> = props => +export const UserProfileView: FC<{}> = () => { const [ userProfile, setUserProfile ] = useState(null); const [ userBadges, setUserBadges ] = useState([]); const [ userRelationships, setUserRelationships ] = useState(null); - const [ activeTab, setActiveTab ] = useState('badge'); - const [ userRooms, setUserRooms ] = useState(null); const onClose = () => { setUserProfile(null); setUserBadges([]); setUserRelationships(null); - setActiveTab('badge'); - setUserRooms(null); }; const onLeaveGroup = () => @@ -35,11 +26,9 @@ export const UserProfileView: FC<{}> = props => GetUserProfile(userProfile.id); }; - const onTabClick = (tab: ProfileTab) => + const onOpenRooms = () => { - setActiveTab(tab); - - if(tab === 'rooms' && !userRooms && userProfile) + if(userProfile) { SendMessageComposer(new NavigatorSearchComposer('hotel_view', `owner:${ userProfile.username }`)); } @@ -80,8 +69,6 @@ export const UserProfileView: FC<{}> = props => { setUserBadges([]); setUserRelationships(null); - setActiveTab('badge'); - setUserRooms(null); } SendMessageComposer(new UserCurrentBadgesComposer(parser.id)); @@ -97,28 +84,6 @@ export const UserProfileView: FC<{}> = props => GetUserProfile(parser.userId); }); - useMessageEvent(NavigatorSearchEvent, event => - { - if(!userProfile || activeTab !== 'rooms') return; - - const parser = event.getParser(); - const result = parser.result; - - if(!result) return; - - const rooms: RoomDataParser[] = []; - - for(const resultList of result.results) - { - if(resultList.rooms && resultList.rooms.length) - { - for(const room of resultList.rooms) rooms.push(room); - } - } - - setUserRooms(rooms); - }); - useNitroEvent(RoomEngineObjectEvent.SELECTED, event => { if(!userProfile) return; @@ -135,82 +100,22 @@ export const UserProfileView: FC<{}> = props => if(!userProfile) return null; return ( - + - -
- + +
+
- - onTabClick('badge') }> - { LocalizeText('levelinfo.category.badge') } - - onTabClick('friends') }> - { LocalizeText('navigator.tab.3') } - - onTabClick('rooms') }> - { LocalizeText('navigator.tab.2') } - - onTabClick('groups') }> - { LocalizeText('navigator.searchcode.title.groups') } - - -
- { activeTab === 'badge' && ( -
- { userBadges && (userBadges.length > 0) - ? userBadges.map((badge, index) => ( - - )) - : ( - - { LocalizeText('extendedprofile.badge.empty') } - - ) - } -
- ) } - { activeTab === 'friends' && ( -
- { userRelationships ? ( - - ) : ( - - { LocalizeText('generic.loading') } - - ) } -
- ) } - { activeTab === 'rooms' && ( -
- { !userRooms && ( - - { LocalizeText('extendedprofile.rooms.loading') } - - ) } - { userRooms && userRooms.length === 0 && ( - - { LocalizeText('extendedprofile.rooms.empty') } - - ) } - { userRooms && userRooms.length > 0 && userRooms.map(room => ( - CreateRoomSession(room.roomId) }> -
- { room.roomName } - { room.description && { room.description } } -
- { room.userCount }/{ room.maxUserCount } -
- )) } -
- ) } - { activeTab === 'groups' && ( -
- -
- ) } +
+
+ +
diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css new file mode 100644 index 0000000..c2c7092 --- /dev/null +++ b/src/css/catalog/CatalogClassicView.css @@ -0,0 +1,395 @@ +.nitro-catalog-classic-window { + width: 606px !important; + height: 565px !important; + max-width: 606px !important; + min-width: 606px !important; + min-height: 565px !important; + max-height: 565px !important; +} + +.nitro-catalog-classic-window .nitro-card-title { + font-size: 18px; + letter-spacing: 0.2px; +} + +.nitro-catalog-classic-window .nitro-card-header-shell { + min-height: 38px; + max-height: 38px; +} + +.nitro-catalog-classic-admin-banner { + border-bottom: 1px solid rgba(0, 0, 0, 0.18); + background: linear-gradient(180deg, #f4d45d 0%, #d8b43e 100%); +} + +.nitro-catalog-classic-tabs-shell { + flex-wrap: nowrap; + gap: 1px; + min-height: 30px; + max-height: 30px; + padding: 0 6px; + overflow-x: auto; + overflow-y: hidden; + align-items: end; + background: #e7e8df; + border-bottom: 1px solid #b8beb4; +} + +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item { + min-height: 28px; + padding: 5px 10px 4px; + border: 1px solid #8f8f8b; + border-bottom: 0; + border-radius: 5px 5px 0 0; + background: linear-gradient(180deg, #fafaf7 0%, #dde2d9 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); + white-space: nowrap; +} + +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item:hover { + background: linear-gradient(180deg, #ffffff 0%, #e7ece4 100%); +} + +.nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { + background: #f2f2eb; + transform: translateY(0); + position: relative; + top: 1px; +} + +.nitro-catalog-classic-content-shell { + padding: 6px 8px 8px !important; +} + +.nitro-catalog-classic-stage { + display: grid; + grid-template-columns: 196px minmax(0, 1fr); + gap: 8px; + min-height: 0; + height: 100%; +} + +.nitro-catalog-classic-stage.is-navigation-hidden { + grid-template-columns: minmax(0, 1fr); +} + +.nitro-catalog-classic-sidebar { + display: flex; + flex-direction: column; + gap: 4px; + min-height: 0; + height: 100%; +} + +.nitro-catalog-classic-search-shell { + padding: 3px; + border: 1px solid #a7aba1; + border-radius: 4px; + background: linear-gradient(180deg, #f9f8f2 0%, #eaede5 100%); +} + +.nitro-catalog-classic-search-shell input { + height: 18px; + padding-top: 0 !important; + padding-bottom: 0 !important; + border-width: 1px !important; + border-color: #8f9588 !important; + border-radius: 3px !important; + background: #fff !important; + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08); +} + +.nitro-catalog-classic-search-shell svg { + color: #61645b !important; +} + +.nitro-catalog-classic-navigation-shell { + flex: 1 1 auto; + min-height: 0; + padding: 3px 2px 3px 3px; + border: 1px solid #a7aba1; + border-radius: 4px; + background: linear-gradient(180deg, #f1f2ec 0%, #d8ddd3 100%); + overflow: auto; +} + +.nitro-catalog-classic-navigation-list { + display: flex; + flex-direction: column; + gap: 2px; +} + +.nitro-catalog-classic-navigation-node.is-child { + margin-left: 10px; +} + +.nitro-catalog-classic-navigation-item { + display: flex; + align-items: center; + gap: 4px; + min-height: 21px; + padding: 1px 6px 1px 5px; + border: 1px solid #bdc2ba; + border-radius: 4px; + background: linear-gradient(180deg, #f6f7f2 0%, #e6e9e1 100%); + color: #2e2e2e; + cursor: pointer; + transition: background-color 0.12s ease, border-color 0.12s ease; +} + +.nitro-catalog-classic-navigation-item:hover { + background: linear-gradient(180deg, #ffffff 0%, #ebeee6 100%); + border-color: #9ea79b; +} + +.nitro-catalog-classic-navigation-item.is-active { + background: linear-gradient(180deg, #dae7f0 0%, #c4d2de 100%); + border-color: #8e9ba5; + font-weight: 700; +} + +.nitro-catalog-classic-navigation-item.is-drag-over { + outline: 2px solid rgba(48, 114, 140, 0.35); + outline-offset: 1px; +} + +.nitro-catalog-classic-navigation-icon { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + min-width: 18px; + height: 18px; +} + +.nitro-catalog-classic-navigation-icon img, +.nitro-catalog-classic-navigation-icon canvas { + width: auto; + height: auto; + max-width: 18px; + max-height: 18px; +} + +.nitro-catalog-classic-navigation-label { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; + line-height: 1; +} + +.nitro-catalog-classic-navigation-caret, +.nitro-catalog-classic-navigation-favorite, +.nitro-catalog-classic-navigation-admin, +.nitro-catalog-classic-navigation-drag { + flex-shrink: 0; +} + +.nitro-catalog-classic-navigation-caret { + color: #676d66 !important; +} + +.nitro-catalog-classic-layout-shell { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + height: 100%; + border: 1px solid #a7aba1; + border-radius: 4px; + background: linear-gradient(180deg, #eceee7 0%, #dfe4da 100%); + overflow: hidden; +} + +.nitro-catalog-classic-layout-header-shell { + display: flex; + flex-direction: column; + gap: 3px; + min-height: 66px; + padding: 5px 7px; + border-bottom: 1px solid #c8cdc3; + background: linear-gradient(180deg, #f6f6f2 0%, #e9ece4 100%); +} + +.nitro-catalog-classic-layout-hero { + display: flex; + align-items: center; + justify-content: center; + flex: 1 1 auto; + min-height: 32px; + overflow: hidden; +} + +.nitro-catalog-classic-layout-hero img { + max-width: 100%; + max-height: 32px; + width: auto; + height: auto; + object-fit: contain; +} + +.nitro-catalog-classic-layout-container { + flex: 1 1 auto; + min-height: 0; + padding: 6px; + background: #f2f2eb; + overflow: hidden; +} + +.nitro-catalog-classic-default-layout { + gap: 8px; +} + +.nitro-catalog-classic-offer-panel, +.nitro-catalog-classic-welcome { + border: 1px solid #bfc4bc; + border-radius: 6px; + background: linear-gradient(180deg, #ffffff 0%, #f3f3ed 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92); +} + +.nitro-catalog-classic-offer-preview { + width: 136px; + min-width: 136px; + padding: 8px; + border-right: 1px solid #c9cec5; + background: linear-gradient(180deg, #eef2ea 0%, #dde3d8 100%); +} + +.nitro-catalog-classic-offer-info { + padding: 10px; +} + +.nitro-catalog-classic-welcome { + min-height: 128px; + padding: 10px; +} + +.nitro-catalog-classic-grid-shell { + min-height: 150px; + padding: 4px; + border: 1px solid #bcc2b8; + border-radius: 6px; + background: linear-gradient(180deg, #f5f5f0 0%, #e4e7de 100%); + height: auto; + flex: 1 1 auto; +} + +.nitro-catalog-classic-grid { + gap: 4px !important; + align-content: start; +} + +.nitro-catalog-classic-window .layout-grid-item { + height: 54px; + border: 1px solid #b8beb6 !important; + border-radius: 6px !important; + background-color: #d7dde2; + background-image: none; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55); +} + +.nitro-catalog-classic-window .layout-grid-item.is-active { + background-color: #e5ebef !important; + border-color: #8f978b !important; +} + +.nitro-catalog-classic-grid-offer-icon { + position: absolute; + inset: 4px; + background-repeat: no-repeat; + background-position: center; + background-size: contain; + pointer-events: none; +} + +.nitro-catalog-classic-window .nitro-catalog-header { + justify-content: flex-start; + min-height: 56px; + margin-bottom: 6px; + padding: 4px 6px; + border: 1px solid #bec3ba; + border-radius: 6px; + background: linear-gradient(180deg, #ffffff 0%, #f2f2ec 100%); +} + +.nitro-catalog-classic-window .nitro-catalog-header img { + max-width: 100%; + max-height: 48px; + width: auto; + height: auto; + object-fit: contain; +} + +.nitro-catalog-classic-breadcrumb { + display: flex; + align-items: center; + gap: 5px; + min-height: 16px; + overflow: hidden; + color: #666a63; + font-size: 10px; + line-height: 1; + white-space: nowrap; +} + +.nitro-catalog-classic-breadcrumb-segment { + display: inline-flex; + align-items: center; + gap: 5px; + min-width: 0; +} + +.nitro-catalog-classic-breadcrumb-separator { + color: #9ea395; +} + +.nitro-catalog-classic-navigation-shell::-webkit-scrollbar, +.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar { + width: 12px; +} + +.nitro-catalog-classic-navigation-shell::-webkit-scrollbar-track, +.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-track { + border-left: 1px solid #c2c6be; + background: #dde2d8; +} + +.nitro-catalog-classic-navigation-shell::-webkit-scrollbar-thumb, +.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-thumb { + border: 1px solid #7d8680; + border-radius: 6px; + background: linear-gradient(180deg, #a8b3ae 0%, #89948f 100%); +} + +.nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:decrement, +.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:decrement { + height: 12px; + background: #dde2d8; +} + +.nitro-catalog-classic-navigation-shell::-webkit-scrollbar-button:single-button:vertical:increment, +.nitro-catalog-classic-layout-container :is(.overflow-auto, .nitro-card-content-shell, .nitro-catalog-classic-grid-shell)::-webkit-scrollbar-button:single-button:vertical:increment { + height: 12px; + background: #dde2d8; +} + +@media (max-width: 991.98px) { + .nitro-catalog-classic-window { + width: min(calc(100vw - 16px), 570px) !important; + min-width: 0 !important; + height: min(calc(100vh - 16px), 635px) !important; + min-height: 0 !important; + max-width: calc(100vw - 16px) !important; + } + + .nitro-catalog-classic-stage { + grid-template-columns: minmax(0, 1fr); + } + + .nitro-catalog-classic-sidebar { + max-height: 180px; + } +} diff --git a/src/css/emustats/EmuStatsView.css b/src/css/emustats/EmuStatsView.css new file mode 100644 index 0000000..2b2ba46 --- /dev/null +++ b/src/css/emustats/EmuStatsView.css @@ -0,0 +1,519 @@ +.nitro-emustats-window { + min-width: 1024px; + max-width: 1024px; + min-height: 700px; + max-height: 700px; +} + +.nitro-emustats-window__content { + padding: 0 !important; + overflow: hidden !important; +} + +.nitro-emustats { + display: grid; + grid-template-columns: 220px minmax(0, 1fr); + height: 100%; + background: #f3f4f8; + color: #1f2937; +} + +.nitro-emustats__sidebar { + display: flex; + flex-direction: column; + min-height: 0; + padding: 18px 14px 14px; + background: linear-gradient(180deg, #f8f9fd 0%, #eef1f8 100%); + border-right: 1px solid #d8deea; +} + +.nitro-emustats__sidebar-brand h2 { + margin: 0; + font-size: 31px; + line-height: 1; + font-weight: 800; + letter-spacing: -0.04em; + color: #1e2a44; +} + +.nitro-emustats__sidebar-brand p { + margin: 8px 0 0; + font-size: 12px; + color: #64748b; +} + +.nitro-emustats__nav { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 22px; +} + +.nitro-emustats__nav-button { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + width: 100%; + padding: 11px 12px; + border: 1px solid #d8deea; + border-radius: 12px; + background: #ffffff; + color: #334155; + cursor: pointer; + transition: background-color 0.18s ease, border-color 0.18s ease, transform 0.18s ease; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); +} + +.nitro-emustats__nav-button:hover { + background: #f8fbff; + border-color: #a9b8db; +} + +.nitro-emustats__nav-button.is-active { + background: linear-gradient(180deg, #e9efff 0%, #dbe7ff 100%); + border-color: #7b93dd; + transform: translateY(-1px); +} + +.nitro-emustats__nav-button span, +.nitro-emustats__nav-button strong { + font-size: 13px; +} + +.nitro-emustats__nav-button strong { + color: #5b6f99; +} + +.nitro-emustats__sidebar-footer { + margin-top: auto; + padding-top: 18px; +} + +.nitro-emustats__refresh-button { + width: 100%; + min-height: 34px; + border: 1px solid #7b93dd; + border-radius: 10px; + background: linear-gradient(180deg, #edf2ff 0%, #dce7ff 100%); + color: #26406d; + font-weight: 700; + cursor: pointer; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); +} + +.nitro-emustats__sidebar-footer p { + margin: 10px 0 0; + font-size: 11px; + text-align: center; + color: #7b879b; +} + +.nitro-emustats__main { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + padding: 22px 24px 20px; + gap: 16px; + background: linear-gradient(180deg, #fbfcff 0%, #f2f5fb 100%); +} + +.nitro-emustats__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.nitro-emustats__header h1 { + margin: 0; + font-size: 24px; + line-height: 1.05; + font-weight: 800; + color: #1e293b; +} + +.nitro-emustats__header p { + margin: 8px 0 0; + font-size: 13px; + color: #64748b; +} + +.nitro-emustats__status-pill { + padding: 7px 12px; + border-radius: 999px; + background: #def7e8; + color: #166534; + font-size: 12px; + font-weight: 700; + white-space: nowrap; + border: 1px solid #a7e0be; +} + +.nitro-emustats__status-pill[data-status="attention-needed"] { + background: #fff1d6; + color: #92400e; + border-color: #f3ce88; +} + +.nitro-emustats__body, +.nitro-emustats__overview { + min-height: 0; + height: 100%; +} + +.nitro-emustats__body { + overflow: auto; + padding-right: 4px; +} + +.nitro-emustats__overview { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 16px; + height: auto; +} + +.nitro-emustats__overview-cards { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + align-content: start; +} + +.nitro-emustats__metric-card { + display: flex; + flex-direction: column; + min-height: 92px; + padding: 12px 13px; + border: 1px solid #dbe2ef; + border-radius: 14px; + background: linear-gradient(180deg, #ffffff 0%, #f8faff 100%); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05); +} + +.nitro-emustats__metric-card::before { + content: ""; + display: block; + width: 24px; + height: 3px; + margin-bottom: 10px; + border-radius: 999px; + background: var(--emustats-accent, #6366f1); +} + +.nitro-emustats__metric-card span { + font-size: 11px; + color: #64748b; +} + +.nitro-emustats__metric-card strong { + margin-top: auto; + font-size: 14px; + line-height: 1.2; + font-weight: 800; + color: #1e293b; + word-break: break-word; +} + +.nitro-emustats__metric-card small { + margin-top: 6px; + font-size: 10px; + color: #94a3b8; +} + +.nitro-emustats__chart-card { + display: flex; + flex-direction: column; + min-height: 290px; + padding: 16px; + border: 1px solid #dbe2ef; + border-radius: 16px; + background: linear-gradient(180deg, #ffffff 0%, #f8faff 100%); + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); +} + +.nitro-emustats__section-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.nitro-emustats__section-header h3 { + margin: 0; + font-size: 16px; + font-weight: 800; + color: #1e293b; +} + +.nitro-emustats__section-header p { + margin: 6px 0 0; + font-size: 12px; + color: #64748b; +} + +.nitro-emustats__chart-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; +} + +.nitro-emustats__chart-meta span { + font-size: 12px; + color: #64748b; +} + +.nitro-emustats__chart-meta strong { + font-size: 22px; + color: #1e293b; +} + +.nitro-emustats__chart-shell { + display: grid; + grid-template-columns: 64px minmax(0, 1fr); + gap: 10px; + min-height: 220px; + height: 220px; +} + +.nitro-emustats__chart-axis { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 10px 0 10px 2px; +} + +.nitro-emustats__chart-axis span { + font-size: 11px; + line-height: 1; + color: #7b879b; +} + +.nitro-emustats__chart-canvas { + position: relative; + min-height: 0; + height: 100%; + border-radius: 14px; + background: #ffffff; + border: 1px solid #dde5f0; + overflow: hidden; +} + +.nitro-emustats__chart { + width: 100%; + height: 100%; +} + +.nitro-emustats__chart-grid { + stroke: #dde5f0; + stroke-width: 0.6; +} + +.nitro-emustats__chart-line { + fill: none; + stroke: #5969d8; + stroke-width: 1.4; + stroke-linecap: round; + stroke-linejoin: round; +} + +.nitro-emustats__chart-hover-line { + stroke: rgba(89, 105, 216, 0.38); + stroke-width: 0.55; + stroke-dasharray: 2 2; +} + +.nitro-emustats__chart-hover-point { + fill: #ffffff; + stroke: #5969d8; + stroke-width: 0.9; +} + +.nitro-emustats__chart-tooltip { + position: absolute; + transform: translate(-50%, -100%); + display: flex; + flex-direction: column; + gap: 2px; + min-width: 92px; + padding: 8px 10px; + border: 1px solid #d9e1ef; + border-radius: 10px; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12); + pointer-events: none; +} + +.nitro-emustats__chart-tooltip strong { + font-size: 12px; + line-height: 1.1; + color: #1e293b; +} + +.nitro-emustats__chart-tooltip span, +.nitro-emustats__chart-tooltip small { + font-size: 10px; + line-height: 1.2; + color: #64748b; +} + +.nitro-emustats__table-shell { + min-height: 0; + height: 100%; + overflow: auto; + border: 1px solid #dbe2ef; + border-radius: 14px; + background: #ffffff; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04); +} + +.nitro-emustats__table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} + +.nitro-emustats__table thead th { + position: sticky; + top: 0; + z-index: 1; + padding: 12px 12px; + text-align: left; + font-size: 12px; + font-weight: 800; + background: #eef3fb; + color: #4b5d7a; + border-bottom: 1px solid #dbe2ef; +} + +.nitro-emustats__table tbody td { + padding: 11px 12px; + font-size: 12px; + border-bottom: 1px solid #edf2f8; + color: #1f2937; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nitro-emustats__table tbody tr:nth-child(even) td { + background: #f8fbff; +} + +.nitro-emustats__table-empty { + padding: 24px 12px !important; + text-align: center; + color: #64748b !important; +} + +.nitro-emustats__table .is-xs { + width: 72px; +} + +.nitro-emustats__table .is-sm { + width: 96px; +} + +.nitro-emustats__table .is-md { + width: 130px; +} + +.nitro-emustats__error, +.nitro-emustats__empty { + padding: 16px 18px; + border: 1px solid #dbe2ef; + border-radius: 12px; + background: #ffffff; + color: #334155; +} + +.nitro-emustats__error { + border-color: #fecaca; + color: #991b1b; + background: #fff5f5; +} + +.nitro-emustats__detail-layout { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + align-content: start; +} + +.nitro-emustats__detail-panel { + display: flex; + flex-direction: column; + min-height: 0; + gap: 12px; + padding: 16px; + border: 1px solid #dbe2ef; + border-radius: 14px; + background: linear-gradient(180deg, #ffffff 0%, #f8faff 100%); + box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04); +} + +.nitro-emustats__detail-panel-header h3 { + margin: 0; + font-size: 15px; + font-weight: 800; + color: #1e293b; +} + +.nitro-emustats__detail-panel-header p { + margin: 6px 0 0; + font-size: 12px; + line-height: 1.4; + color: #64748b; +} + +.nitro-emustats__kv-grid { + display: grid; + gap: 10px; +} + +.nitro-emustats__kv-grid.is-2col { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.nitro-emustats__kv-grid.is-1col { + grid-template-columns: minmax(0, 1fr); +} + +.nitro-emustats__kv-item { + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border: 1px solid #e5eaf4; + border-radius: 12px; + background: #ffffff; +} + +.nitro-emustats__kv-item span { + font-size: 11px; + color: #64748b; +} + +.nitro-emustats__kv-item strong { + font-size: 14px; + line-height: 1.25; + color: #1e293b; + word-break: break-word; +} + +.nitro-emustats__detail-panel .nitro-emustats__table-shell { + min-height: 220px; + max-height: 220px; +} + +.nitro-emustats__kv-item strong[data-tone="good"] { + color: #15803d; +} + +.nitro-emustats__kv-item strong[data-tone="warn"] { + color: #b45309; +} diff --git a/src/css/user-profile/UserProfileView.css b/src/css/user-profile/UserProfileView.css new file mode 100644 index 0000000..3f3e47b --- /dev/null +++ b/src/css/user-profile/UserProfileView.css @@ -0,0 +1,487 @@ +.nitro-extended-profile-window { + border-radius: 0 !important; +} + +.nitro-extended-profile-window .nitro-card-header-shell { + min-height: 34px; + max-height: 34px; +} + +.nitro-extended-profile-window .nitro-card-title { + font-size: 25px; + line-height: 1; + text-shadow: 1px 1px 0 #4f4f4f; +} + +.nitro-extended-profile-window__content { + background: #ece8dc; +} + +.nitro-extended-profile-window .nitro-card-close-button { + right: 6px; +} + +.nitro-extended-profile { + display: flex; + flex-direction: column; + gap: 7px; +} + +.nitro-extended-profile__top { + display: grid; + grid-template-columns: minmax(0, 1.06fr) 1px minmax(0, 0.94fr); + gap: 10px; + min-height: 188px; +} + +.nitro-extended-profile__separator { + background: #afafaf; +} + +.nitro-extended-profile__left, +.nitro-extended-profile__right { + display: flex; + flex-direction: column; +} + +.nitro-extended-profile__identity { + display: grid; + grid-template-columns: 56px minmax(0, 1fr); + gap: 8px; +} + +.nitro-extended-profile__avatar-shell { + width: 56px; + height: 113px; + position: relative; + overflow: hidden; + background-size: cover; + background-position: center; +} + +.nitro-extended-profile__avatar-stand, +.nitro-extended-profile__avatar-overlay { + position: absolute; + inset: 0; +} + +.nitro-extended-profile__avatar-image { + position: absolute !important; + left: 50% !important; + bottom: -4px; + transform: translateX(-50%); + width: auto !important; + height: auto !important; +} + +.nitro-extended-profile__identity-copy { + display: flex; + flex-direction: column; + min-width: 0; +} + +.nitro-extended-profile__username { + display: inline-flex; + align-items: center; + min-width: 0; + max-width: 100%; + font-size: 16px; + font-weight: 700; + line-height: 16px; +} + +.nitro-extended-profile__motto { + margin: 0; + min-height: 24px; + color: #242424; + font-size: 11px; + font-style: italic; + line-height: 12px; + overflow: hidden; +} + +.nitro-extended-profile__meta { + margin: 0; + color: #111; + font-size: 11px; + line-height: 15px; +} + +.nitro-extended-profile__meta--strong { + margin-top: 2px; +} + +.nitro-extended-profile__status { + display: flex; + align-items: center; + gap: 5px; + min-height: 23px; + margin-top: 3px; +} + +.nitro-extended-profile__presence { + width: 40px; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.nitro-extended-profile__status-copy { + min-height: 23px; + display: flex; + align-items: center; + gap: 4px; + min-width: 0; +} + +.nitro-extended-profile__status-text { + color: #111; + font-size: 11px; + font-weight: 700; + line-height: 1; +} + +.nitro-extended-profile__friend-button, +.nitro-extended-profile__link { + border: 0; + background: none; + padding: 0; + color: #0655b7; + font-size: 11px; + line-height: 1; + text-decoration: underline; + cursor: pointer; +} + +.nitro-extended-profile__friend-button { + min-height: 24px; + padding: 0 14px; + border: 1px solid #9b9b9b; + border-radius: 4px; + background: linear-gradient(180deg, #ffffff 0%, #e2e2e2 100%); + color: #222; + text-decoration: none; +} + +.nitro-extended-profile__actions { + display: flex; + gap: 12px; + margin-top: 2px; + min-height: 16px; +} + +.nitro-extended-profile__badges { + margin-top: 5px; + min-height: 55px; + border: 1px solid #afafaf; + background: linear-gradient(180deg, #f6f3e7 0%, #ede8d8 100%); + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0; + padding: 6px 7px; +} + +.nitro-extended-profile__badge-slot { + width: 42px; + height: 42px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 0; + padding: 0; + cursor: pointer; +} + +.nitro-extended-profile__badge-slot .badge-image { + width: auto !important; + height: auto !important; +} + +.nitro-extended-profile__right { + padding-top: 6px; +} + +.nitro-extended-profile__relationships-label { + margin: 3px 0 4px; + color: #111; + font-size: 12px; + font-weight: 700; + line-height: 16px; +} + +.nitro-extended-profile__relationship { + display: grid; + grid-template-columns: 19px minmax(0, 1fr); + gap: 4px; + margin-bottom: 4px; +} + +.nitro-extended-profile__relationship-icon { + height: 33px; +} + +.nitro-extended-profile__relationship-copy { + display: flex; + flex-direction: column; + min-width: 0; +} + +.nitro-extended-profile__relationship-box { + min-height: 22px; + border: 1px solid #b4b4b4; + background: linear-gradient(180deg, #fdfdfb 0%, #ecebe3 100%); + display: flex; + align-items: center; + padding: 0 7px; + position: relative; + overflow: hidden; +} + +.nitro-extended-profile__relationship-name { + margin: 0; + color: #111; + font-size: 11px; + font-weight: 700; + text-decoration: underline; + cursor: pointer; + max-width: calc(100% - 38px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nitro-extended-profile__relationship-head { + position: absolute; + right: -2px; + top: 50%; + width: 34px; + height: 34px; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; +} + +.nitro-extended-profile__relationship-subcopy { + margin: 1px 0 0 8px; + color: #7f7f7f; + font-size: 11px; + font-style: italic; + line-height: 14px; +} + +.nitro-extended-profile__summary-bar { + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: center; + min-height: 38px; + background: #ece8dc; + border-top: 1px solid #afafaf; + border-bottom: 1px solid #afafaf; +} + +.nitro-extended-profile__summary-button { + min-height: 38px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 0 10px; + background: transparent; + border: 0; + border-right: 1px solid #afafaf; + color: #111; + font-size: 11px; +} + +.nitro-extended-profile__summary-button:first-child { + border-left: 1px solid #afafaf; +} + +.nitro-extended-profile__summary-button--center { + cursor: pointer; +} + +.nitro-extended-profile__summary-icon { + width: auto; + height: auto; + max-width: 32px; + max-height: 28px; + image-rendering: pixelated; + flex-shrink: 0; +} + +.nitro-extended-profile__summary-icon--badge { + max-width: 25px; + max-height: 25px; +} + +.nitro-extended-profile__summary-label { + font-weight: 700; + line-height: 1; + text-decoration: underline; +} + +.nitro-extended-profile__summary-value { + font-weight: 700; + line-height: 1; +} + +.nitro-extended-profile-window__body { + background: #ece8dc; +} + +.nitro-extended-profile-window__body--groups { + min-height: 249px; +} + +.nitro-extended-profile-window__panel { + border: 1px solid #afafaf; + background: #d6d3cb; + padding: 0 !important; +} + +.nitro-extended-profile-groups { + display: grid; + grid-template-columns: 82px minmax(0, 1fr); + gap: 7px; + min-height: 236px; + height: 100%; +} + +.nitro-extended-profile-groups__sidebar { + display: flex; + flex-direction: column; + min-width: 0; +} + +.nitro-extended-profile-groups__count { + margin-bottom: 5px; + color: #111; + font-size: 11px; + line-height: 1; +} + +.nitro-extended-profile-groups__list { + flex: 1 1 auto; + overflow-y: auto; + padding-right: 2px; + scrollbar-width: thin; +} + +.nitro-extended-profile-groups__item { + width: 54px; + min-height: 54px; + margin-bottom: 4px; +} + +.nitro-extended-profile-groups__details { + min-width: 0; + min-height: 236px; + border: 1px solid #9e9e9e; + border-radius: 14px; + background: #bdbbbb; + padding: 11px; +} + +.nitro-extended-profile-group-info { + display: grid; + grid-template-columns: 110px minmax(0, 1fr); + gap: 12px; + min-height: 100%; + border: 1px solid #9f9f9f; + border-radius: 10px; + background: #efede4; + padding: 12px 14px; +} + +.nitro-extended-profile-group-info__badge-column { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.nitro-extended-profile-group-info__badge-wrap { + width: 96px; + height: 96px; + display: flex; + align-items: center; + justify-content: center; +} + +.nitro-extended-profile-group-info__meta { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + text-align: center; +} + +.nitro-extended-profile-group-info__role { + min-height: 18px; + display: flex; + align-items: center; + justify-content: center; +} + +.nitro-extended-profile-group-info__content { + display: flex; + flex-direction: column; + min-width: 0; +} + +.nitro-extended-profile-group-info__header-copy { + display: flex; + flex-direction: column; + gap: 3px; +} + +.nitro-extended-profile-group-info__description { + margin-top: 10px; + min-height: 52px; + color: #222; + line-height: 14px; +} + +.nitro-extended-profile-group-info__links { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 8px; +} + +.nitro-extended-profile-group-info__button { + align-self: center; + min-width: 162px; + min-height: 24px; + margin-top: auto; +} + +.nitro-extended-profile-group-info .group-badge .badge-image { + transform: scale(2.1); + transform-origin: center; +} + +.nitro-extended-profile-window .layout-grid-item.active { + background: linear-gradient(180deg, #ffe89b 0%, #ffc74b 100%); + box-shadow: inset 0 0 0 2px #fff4c4; +} + +.nitro-extended-profile-window .layout-grid-item { + border-radius: 4px; + border: 1px solid #8d8d8d; + background: #efede4; +} + +.nitro-extended-profile-groups__item .layout-grid-item-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.nitro-extended-profile-window .layout-grid-item .badge-image.group-badge { + width: auto !important; + height: auto !important; +} diff --git a/src/index.tsx b/src/index.tsx index 7a8b018..3af6732 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,6 +19,8 @@ import './css/index.css'; import './css/backgrounds/BackgroundsView.css'; import './css/badges/BadgeLeaderboardView.css'; +import './css/catalog/CatalogClassicView.css'; +import './css/emustats/EmuStatsView.css'; import './css/chat/Chats.css'; @@ -52,6 +54,7 @@ import './css/room/RoomWidgets.css'; import './css/slider.css'; import './css/toolbar/ToolBar.css'; +import './css/user-profile/UserProfileView.css'; import './css/widgets/FurnitureWidgets.css';