Merge pull request #136 from Lorenzune/merge-duckie-main-2026-05-06
Add badge leaderboard UI and rarity styling
@@ -1,4 +1,4 @@
|
||||
# v2.2.0 -Nitro V3 !! Use at Own Risk as it is still in Beta !!
|
||||
# Nitro V3
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"gamedata.url": "https://nitro.example.com:2096/nitro-sec/file?kind=gamedata&file=",
|
||||
"sounds.url": "${asset.url}/sounds/%sample%.mp3",
|
||||
"external.texts.url": [
|
||||
"${gamedata.url}/ExternalTexts.json",
|
||||
"${gamedata.url}/UITexts.json"
|
||||
"${gamedata.url}/ExternalTexts.json?t=%timestamp%",
|
||||
"${gamedata.url}/UITexts.json?t=%timestamp%"
|
||||
],
|
||||
"external.texts.translation.url": "${gamedata.url}/text_translate/ExternalTexts_%locale%.json?t=%timestamp%",
|
||||
"external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3",
|
||||
@@ -68,6 +68,7 @@
|
||||
"badges.custom.update.endpoint": "${api.url}/api/badges/custom/%badgeId%",
|
||||
"badges.custom.delete.endpoint": "${api.url}/api/badges/custom/%badgeId%",
|
||||
"badges.custom.texts.endpoint": "${api.url}/api/badges/custom/texts",
|
||||
"badges.leaderboard.endpoint": "${api.url}/api/badges/leaderboard",
|
||||
"login.turnstile.enabled": true,
|
||||
"login.turnstile.sitekey": "1x00000000000000000000AA",
|
||||
"avatar.mandatory.libraries": [
|
||||
@@ -612,4 +613,4 @@
|
||||
"${images.url}/clear_icon.png",
|
||||
"${images.url}/big_arrow.png"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { GetConfiguration } from '@nitrots/nitro-renderer';
|
||||
import { getAccessToken } from '../auth';
|
||||
|
||||
export type BadgeRarityKey = 'common' | 'rare' | 'epic' | 'legendary' | 'mythical' | 'unique';
|
||||
|
||||
export interface BadgeLeaderboardStat
|
||||
{
|
||||
badgeCode: string;
|
||||
ownerCount: number;
|
||||
rarity: BadgeRarityKey;
|
||||
}
|
||||
|
||||
export interface BadgeLeaderboardEntry
|
||||
{
|
||||
userId: number;
|
||||
username: string;
|
||||
figure: string;
|
||||
score: number;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export interface BadgeLeaderboardBoard
|
||||
{
|
||||
entries: BadgeLeaderboardEntry[];
|
||||
totalPlayers: number;
|
||||
viewerEntry?: Partial<BadgeLeaderboardEntry>;
|
||||
}
|
||||
|
||||
export interface BadgeLeaderboardResponse
|
||||
{
|
||||
viewerUserId: number;
|
||||
badgeStats: BadgeLeaderboardStat[];
|
||||
thresholds: {
|
||||
commonMinOwners: number;
|
||||
rareMinOwners: number;
|
||||
epicMinOwners: number;
|
||||
legendaryMinOwners: number;
|
||||
mythicalMinOwners: number;
|
||||
uniqueOwners: number;
|
||||
};
|
||||
leaderboards: {
|
||||
totalBadges: BadgeLeaderboardBoard;
|
||||
achievementLevel: BadgeLeaderboardBoard;
|
||||
rarity: Record<BadgeRarityKey, BadgeLeaderboardBoard>;
|
||||
};
|
||||
}
|
||||
|
||||
const interpolate = (value: string): string =>
|
||||
{
|
||||
try { return GetConfiguration().interpolate(value); }
|
||||
catch { return value; }
|
||||
};
|
||||
|
||||
const getUrl = (): string =>
|
||||
{
|
||||
const configured = GetConfiguration().getValue<string>('badges.leaderboard.endpoint', '/api/badges/leaderboard');
|
||||
|
||||
return interpolate(configured);
|
||||
};
|
||||
|
||||
const authHeaders = (): Record<string, string> =>
|
||||
{
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'NitroBadgeLeaderboard'
|
||||
};
|
||||
|
||||
const token = getAccessToken();
|
||||
|
||||
if(token) headers.Authorization = `Bearer ${ token }`;
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
const parseJson = async <T>(response: Response): Promise<T> =>
|
||||
{
|
||||
const text = await response.text();
|
||||
|
||||
if(!text) return {} as T;
|
||||
|
||||
try { return JSON.parse(text) as T; }
|
||||
catch { throw new Error('Invalid response from badge leaderboard endpoint.'); }
|
||||
};
|
||||
|
||||
const throwOnError = async (response: Response): Promise<void> =>
|
||||
{
|
||||
if(response.ok) return;
|
||||
|
||||
const payload = await parseJson<{ error?: string }>(response);
|
||||
const message = payload?.error || `Request failed (${ response.status }).`;
|
||||
const error = new Error(message) as Error & { status?: number };
|
||||
|
||||
error.status = response.status;
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
let cachePromise: Promise<BadgeLeaderboardResponse> = null;
|
||||
let cacheValue: BadgeLeaderboardResponse = null;
|
||||
|
||||
const buildStatsMap = (response: BadgeLeaderboardResponse | null): Map<string, BadgeLeaderboardStat> =>
|
||||
{
|
||||
const map = new Map<string, BadgeLeaderboardStat>();
|
||||
|
||||
if(!response?.badgeStats?.length) return map;
|
||||
|
||||
for(const stat of response.badgeStats)
|
||||
{
|
||||
if(!stat?.badgeCode) continue;
|
||||
|
||||
map.set(stat.badgeCode, stat);
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
let cacheStatsMap: Map<string, BadgeLeaderboardStat> = new Map();
|
||||
|
||||
export const fetchBadgeLeaderboard = async (force = false): Promise<BadgeLeaderboardResponse> =>
|
||||
{
|
||||
if(!force)
|
||||
{
|
||||
if(cacheValue) return cacheValue;
|
||||
if(cachePromise) return cachePromise;
|
||||
}
|
||||
|
||||
cachePromise = (async () =>
|
||||
{
|
||||
const response = await fetch(getUrl(), {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: authHeaders()
|
||||
});
|
||||
|
||||
await throwOnError(response);
|
||||
|
||||
const payload = await parseJson<BadgeLeaderboardResponse>(response);
|
||||
|
||||
cacheValue = payload;
|
||||
cacheStatsMap = buildStatsMap(payload);
|
||||
|
||||
return payload;
|
||||
})();
|
||||
|
||||
try
|
||||
{
|
||||
return await cachePromise;
|
||||
}
|
||||
finally
|
||||
{
|
||||
cachePromise = null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCachedBadgeLeaderboard = (): BadgeLeaderboardResponse =>
|
||||
{
|
||||
return cacheValue;
|
||||
};
|
||||
|
||||
export const getCachedBadgeRarityStat = (badgeCode: string): BadgeLeaderboardStat =>
|
||||
{
|
||||
if(!badgeCode) return null;
|
||||
|
||||
return cacheStatsMap.get(badgeCode) || null;
|
||||
};
|
||||
|
||||
export const ensureBadgeLeaderboardLoaded = async (): Promise<BadgeLeaderboardResponse> =>
|
||||
{
|
||||
return fetchBadgeLeaderboard(false);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export * from './CustomBadgeApi';
|
||||
export * from './BadgeLeaderboardApi';
|
||||
|
||||
|
After Width: | Height: | Size: 654 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 975 B |
|
After Width: | Height: | Size: 787 B |
|
After Width: | Height: | Size: 787 B |
|
After Width: | Height: | Size: 787 B |
|
After Width: | Height: | Size: 871 B |
|
After Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 966 B |
|
After Width: | Height: | Size: 787 B |
|
After Width: | Height: | Size: 930 B |
|
After Width: | Height: | Size: 787 B |
|
After Width: | Height: | Size: 847 B |
|
After Width: | Height: | Size: 798 B |
|
After Width: | Height: | Size: 1016 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,32 @@
|
||||
export { default as badgeEmblemAchievement } from './badge_emblem_achievement.png';
|
||||
export { default as badgeEmblemAchievementExtended } from './badge_emblem_achievement_extended.png';
|
||||
export { default as badgeEmblemCommon } from './badge_emblem_common.png';
|
||||
export { default as badgeEmblemCommonExtended } from './badge_emblem_common_extended.png';
|
||||
export { default as badgeEmblemDefault } from './badge_emblem_default.png';
|
||||
export { default as badgeEmblemEpic } from './badge_emblem_epic.png';
|
||||
export { default as badgeEmblemEpicExtended } from './badge_emblem_epic_extended.png';
|
||||
export { default as badgeEmblemLegendary } from './badge_emblem_legendary.png';
|
||||
export { default as badgeEmblemLegendaryExtended } from './badge_emblem_legendary_extended.png';
|
||||
export { default as badgeEmblemMythical } from './badge_emblem_mythical.png';
|
||||
export { default as badgeEmblemMythicalExtended } from './badge_emblem_mythical_extended.png';
|
||||
export { default as badgeEmblemRare } from './badge_emblem_rare.png';
|
||||
export { default as badgeEmblemRareExtended } from './badge_emblem_rare_extended.png';
|
||||
export { default as badgeEmblemUnique } from './badge_emblem_unique.png';
|
||||
export { default as badgeEmblemUniqueExtended } from './badge_emblem_unique_extended.png';
|
||||
export { default as frameLeaderboardAchievement } from './frame_leaderboard_achievement.png';
|
||||
export { default as frameLeaderboardRarityCommon } from './frame_leaderboard_rarity_common.png';
|
||||
export { default as frameLeaderboardRarityEpic } from './frame_leaderboard_rarity_epic.png';
|
||||
export { default as frameLeaderboardRarityLegendary } from './frame_leaderboard_rarity_legendary.png';
|
||||
export { default as frameLeaderboardRarityMythical } from './frame_leaderboard_rarity_mythical.png';
|
||||
export { default as frameLeaderboardRarityRare } from './frame_leaderboard_rarity_rare.png';
|
||||
export { default as frameLeaderboardRarityUnique } from './frame_leaderboard_rarity_unique.png';
|
||||
export { default as frameLeaderboardTotal } from './frame_leaderboard_total.png';
|
||||
export { default as leaderboardBadgesWhite } from './leaderboard_badges_white.png';
|
||||
export { default as leaderboardButtonCloseSwf } from './leaderboard_button_close_swf.png';
|
||||
export { default as leaderboardDropdownOpener } from './leaderboard_dropdown_opener.png';
|
||||
export { default as leaderboardDivider } from './leaderboard_divider.png';
|
||||
export { default as leaderboardEntryEven } from './leaderboard_entry_even.png';
|
||||
export { default as leaderboardEntrySelf } from './leaderboard_entry_self.png';
|
||||
export { default as leaderboardEntryUneven } from './leaderboard_entry_uneven.png';
|
||||
export { default as leaderboardHighlighter } from './leaderboard_highlighter.png';
|
||||
export { default as leaderboardIconProgress } from './leaderboard_icon_progress.png';
|
||||
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 710 B |
|
After Width: | Height: | Size: 710 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 698 B |
|
After Width: | Height: | Size: 729 B |
|
After Width: | Height: | Size: 867 B |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 787 B |
@@ -0,0 +1,4 @@
|
||||
raro > 10
|
||||
epico > 6
|
||||
leggendario > 3
|
||||
unico > 0
|
||||
|
After Width: | Height: | Size: 72 KiB |
@@ -30,7 +30,6 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
|
||||
const [isPositioned, setIsPositioned] = useState(false);
|
||||
const [dragHandler, setDragHandler] = useState<HTMLElement>(null);
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
const bringToTop = useCallback(() => {
|
||||
let zIndex = 400;
|
||||
for (const existingWindow of CURRENT_WINDOWS) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BadgeImageReadyEvent, GetEventDispatcher, GetSessionDataManager, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { GetConfigurationValue, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../api';
|
||||
import { BadgeLeaderboardStat, ensureBadgeLeaderboardLoaded, getCachedBadgeRarityStat, GetConfigurationValue, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../api';
|
||||
import { Base, BaseProps } from '../Base';
|
||||
|
||||
export interface LayoutBadgeImageViewProps extends BaseProps<HTMLDivElement>
|
||||
@@ -12,13 +12,25 @@ export interface LayoutBadgeImageViewProps extends BaseProps<HTMLDivElement>
|
||||
customTitle?: string;
|
||||
isGrayscale?: boolean;
|
||||
scale?: number;
|
||||
showRarityInfo?: boolean;
|
||||
highlightRarity?: boolean;
|
||||
}
|
||||
|
||||
const BADGE_RARITY_COLORS: Record<string, { glow: string; pillBackground: string; pillBorder: string; pillText: string }> = {
|
||||
common: { glow: 'rgba(148, 163, 184, 0.55)', pillBackground: 'rgba(71, 85, 105, 0.16)', pillBorder: 'rgba(100, 116, 139, 0.45)', pillText: '#475569' },
|
||||
rare: { glow: 'rgba(59, 130, 246, 0.7)', pillBackground: 'rgba(59, 130, 246, 0.12)', pillBorder: 'rgba(37, 99, 235, 0.38)', pillText: '#1d4ed8' },
|
||||
epic: { glow: 'rgba(168, 85, 247, 0.72)', pillBackground: 'rgba(168, 85, 247, 0.14)', pillBorder: 'rgba(147, 51, 234, 0.4)', pillText: '#7e22ce' },
|
||||
legendary: { glow: 'rgba(249, 115, 22, 0.76)', pillBackground: 'rgba(249, 115, 22, 0.16)', pillBorder: 'rgba(234, 88, 12, 0.4)', pillText: '#c2410c' },
|
||||
mythical: { glow: 'rgba(236, 72, 153, 0.76)', pillBackground: 'rgba(236, 72, 153, 0.15)', pillBorder: 'rgba(219, 39, 119, 0.4)', pillText: '#be185d' },
|
||||
unique: { glow: 'rgba(34, 197, 94, 0.76)', pillBackground: 'rgba(34, 197, 94, 0.14)', pillBorder: 'rgba(22, 163, 74, 0.4)', pillText: '#15803d' }
|
||||
};
|
||||
|
||||
export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
{
|
||||
const { badgeCode = null, isGroup = false, showInfo = false, customTitle = null, isGrayscale = false, scale = 1, classNames = [], style = {}, children = null, ...rest } = props;
|
||||
const { badgeCode = null, isGroup = false, showInfo = false, customTitle = null, isGrayscale = false, scale = 1, showRarityInfo = false, highlightRarity = false, classNames = [], style = {}, children = null, ...rest } = props;
|
||||
const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
|
||||
const [ tooltipPosition, setTooltipPosition ] = useState<{ top: number; left: number } | null>(null);
|
||||
const [ badgeRarityStat, setBadgeRarityStat ] = useState<BadgeLeaderboardStat>(null);
|
||||
const badgeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const tooltipsEnabled = showInfo && GetConfigurationValue<boolean>('badge.descriptions.enabled', true);
|
||||
@@ -73,10 +85,21 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
}
|
||||
}
|
||||
|
||||
if(highlightRarity && badgeRarityStat)
|
||||
{
|
||||
const colors = BADGE_RARITY_COLORS[badgeRarityStat.rarity];
|
||||
|
||||
if(colors)
|
||||
{
|
||||
newStyle.borderRadius = 8;
|
||||
newStyle.boxShadow = `0 0 0 1px ${ colors.glow }, 0 0 14px ${ colors.glow }`;
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ badgeCode, isGroup, imageElement, scale, style ]);
|
||||
}, [ badgeCode, badgeRarityStat, highlightRarity, isGroup, imageElement, scale, style ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@@ -136,6 +159,46 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
return () => GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
|
||||
}, [ badgeCode, isGroup ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(isGroup || !badgeCode || (!showRarityInfo && !highlightRarity))
|
||||
{
|
||||
setBadgeRarityStat(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = getCachedBadgeRarityStat(badgeCode);
|
||||
|
||||
if(cached)
|
||||
{
|
||||
setBadgeRarityStat(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
ensureBadgeLeaderboardLoaded()
|
||||
.then(() =>
|
||||
{
|
||||
if(cancelled) return;
|
||||
|
||||
setBadgeRarityStat(getCachedBadgeRarityStat(badgeCode));
|
||||
})
|
||||
.catch(() =>
|
||||
{
|
||||
if(cancelled) return;
|
||||
|
||||
setBadgeRarityStat(null);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [ badgeCode, highlightRarity, isGroup, showRarityInfo ]);
|
||||
|
||||
const rarityColors = badgeRarityStat ? BADGE_RARITY_COLORS[badgeRarityStat.rarity] : null;
|
||||
const rarityLabel = badgeRarityStat ? LocalizeText(`badge.rarity.${ badgeRarityStat.rarity }`) : '';
|
||||
const rarityText = badgeRarityStat ? LocalizeText('badge.rarity.badge', [ 'rarity' ], [ rarityLabel ]) : '';
|
||||
const ownersText = badgeRarityStat ? LocalizeText('badge.owner_count', [ 'count' ], [ badgeRarityStat.ownerCount.toString() ]) : '';
|
||||
|
||||
return (
|
||||
<Base
|
||||
innerRef={ badgeRef }
|
||||
@@ -149,6 +212,19 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
className="fixed z-[9999] pointer-events-none select-none w-[210px] rounded-[.25rem] bg-[#fff] text-black py-1 px-2 small"
|
||||
style={ { top: tooltipPosition.top, left: tooltipPosition.left } }>
|
||||
<div className="font-bold mb-1">{ isGroup ? customTitle : LocalizeBadgeName(badgeCode) }</div>
|
||||
{ showRarityInfo && badgeRarityStat &&
|
||||
<div className="flex flex-col gap-1 mb-1">
|
||||
<div
|
||||
className="inline-flex items-center self-start rounded-full px-2 py-[2px] text-[10px] font-bold uppercase tracking-[0.04em]"
|
||||
style={ {
|
||||
background: rarityColors.pillBackground,
|
||||
border: `1px solid ${ rarityColors.pillBorder }`,
|
||||
color: rarityColors.pillText
|
||||
} }>
|
||||
{ rarityText }
|
||||
</div>
|
||||
<div className="text-[10px] text-[#5f5f5f]">{ ownersText }</div>
|
||||
</div> }
|
||||
<div>{ isGroup ? LocalizeText('group.badgepopup.body') : LocalizeBadgeDescription(badgeCode) }</div>
|
||||
</div>,
|
||||
document.body
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useNitroEvent } from '../hooks';
|
||||
import { AchievementsView } from './achievements/AchievementsView';
|
||||
import { AvatarEditorView } from './avatar-editor';
|
||||
import { BadgeCreatorView } from './badge-creator';
|
||||
import { BadgeLeaderboardView } from './badge-leaderboard/BadgeLeaderboardView';
|
||||
import { AvatarEffectsView } from './avatar-effects';
|
||||
import { CameraWidgetView } from './camera/CameraWidgetView';
|
||||
import { CampaignView } from './campaign/CampaignView';
|
||||
@@ -125,6 +126,7 @@ export const MainView: FC<{}> = props =>
|
||||
<WiredView />
|
||||
<AvatarEditorView />
|
||||
<BadgeCreatorView />
|
||||
<BadgeLeaderboardView />
|
||||
<AvatarEffectsView />
|
||||
<AchievementsView />
|
||||
<NavigatorView />
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { CSSProperties, FC, useEffect, useMemo, useState } from 'react';
|
||||
import { BadgeLeaderboardBoard, BadgeLeaderboardEntry, BadgeRarityKey, fetchBadgeLeaderboard, getCachedBadgeLeaderboard, LocalizeText } from '../../api';
|
||||
import { Column, DraggableWindow, DraggableWindowPosition, Flex, Text } from '../../common';
|
||||
import {
|
||||
badgeEmblemAchievement,
|
||||
badgeEmblemCommon,
|
||||
badgeEmblemDefault,
|
||||
badgeEmblemEpic,
|
||||
badgeEmblemLegendary,
|
||||
badgeEmblemMythical,
|
||||
badgeEmblemRare,
|
||||
badgeEmblemUnique,
|
||||
frameLeaderboardAchievement,
|
||||
frameLeaderboardRarityCommon,
|
||||
frameLeaderboardRarityEpic,
|
||||
frameLeaderboardRarityLegendary,
|
||||
frameLeaderboardRarityMythical,
|
||||
frameLeaderboardRarityRare,
|
||||
frameLeaderboardRarityUnique,
|
||||
frameLeaderboardTotal,
|
||||
leaderboardButtonCloseSwf,
|
||||
leaderboardDivider,
|
||||
leaderboardDropdownOpener
|
||||
} from '../../assets/images/leaderboard_badge';
|
||||
|
||||
type LeaderboardPage =
|
||||
| { key: 'totalBadges'; board: BadgeLeaderboardBoard; frame: string; emblem: string; title: () => string; info: () => string; option: () => string; }
|
||||
| { key: 'achievementLevel'; board: BadgeLeaderboardBoard; frame: string; emblem: string; title: () => string; info: () => string; option: () => string; }
|
||||
| { key: `rarity-${ BadgeRarityKey }`; rarity: BadgeRarityKey; board: BadgeLeaderboardBoard; frame: string; emblem: string; title: () => string; info: () => string; option: () => string; };
|
||||
|
||||
const RARITY_ASSETS: Record<BadgeRarityKey, { frame: string; emblem: string }> = {
|
||||
common: { frame: frameLeaderboardRarityCommon, emblem: badgeEmblemCommon },
|
||||
rare: { frame: frameLeaderboardRarityRare, emblem: badgeEmblemRare },
|
||||
epic: { frame: frameLeaderboardRarityEpic, emblem: badgeEmblemEpic },
|
||||
legendary: { frame: frameLeaderboardRarityLegendary, emblem: badgeEmblemLegendary },
|
||||
mythical: { frame: frameLeaderboardRarityMythical, emblem: badgeEmblemMythical },
|
||||
unique: { frame: frameLeaderboardRarityUnique, emblem: badgeEmblemUnique }
|
||||
};
|
||||
|
||||
const RARITY_ORDER: BadgeRarityKey[] = [ 'common', 'rare', 'epic', 'legendary', 'mythical', 'unique' ];
|
||||
const PAGE_SIZE = 10;
|
||||
const getAvatarHeadUrl = (figure: string): string =>
|
||||
{
|
||||
if(!figure) return '';
|
||||
|
||||
return `https://www.habbo.com/habbo-imaging/avatarimage?figure=${ encodeURIComponent(figure) }&direction=2&head_direction=2&gesture=sml&size=m&headonly=1`;
|
||||
};
|
||||
|
||||
export const BadgeLeaderboardView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
const [ loadError, setLoadError ] = useState<string>(null);
|
||||
const [ version, setVersion ] = useState(0);
|
||||
const [ categoryIndex, setCategoryIndex ] = useState(0);
|
||||
const [ entryPageIndex, setEntryPageIndex ] = useState(0);
|
||||
const [ isCategoryMenuVisible, setIsCategoryMenuVisible ] = useState(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
setIsVisible(value => !value);
|
||||
return;
|
||||
case 'refresh':
|
||||
setVersion(value => value + 1);
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'badge-leaderboard/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
setIsLoading(true);
|
||||
setLoadError(null);
|
||||
|
||||
fetchBadgeLeaderboard(version > 0)
|
||||
.then(() =>
|
||||
{
|
||||
if(cancelled) return;
|
||||
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(error =>
|
||||
{
|
||||
if(cancelled) return;
|
||||
|
||||
setLoadError(String((error as Error)?.message || error));
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [ isVisible, version ]);
|
||||
|
||||
const leaderboard = getCachedBadgeLeaderboard();
|
||||
|
||||
const pages = useMemo<LeaderboardPage[]>(() =>
|
||||
{
|
||||
if(!leaderboard) return [];
|
||||
|
||||
const built: LeaderboardPage[] = [
|
||||
{
|
||||
key: 'totalBadges',
|
||||
board: leaderboard.leaderboards.totalBadges,
|
||||
frame: frameLeaderboardTotal,
|
||||
emblem: badgeEmblemDefault,
|
||||
title: () => LocalizeText('badge_leaderboard.title.total_badges'),
|
||||
info: () => LocalizeText('badge_leaderboard.info.total_badges'),
|
||||
option: () => LocalizeText('badge_leaderboard.option.total_badges')
|
||||
},
|
||||
{
|
||||
key: 'achievementLevel',
|
||||
board: leaderboard.leaderboards.achievementLevel,
|
||||
frame: frameLeaderboardAchievement,
|
||||
emblem: badgeEmblemAchievement,
|
||||
title: () => LocalizeText('badge_leaderboard.title.achievement_level'),
|
||||
info: () => LocalizeText('badge_leaderboard.info.achievement_level'),
|
||||
option: () => LocalizeText('badge_leaderboard.option.achievement_level')
|
||||
}
|
||||
];
|
||||
|
||||
for(const rarity of RARITY_ORDER)
|
||||
{
|
||||
const board = leaderboard.leaderboards.rarity?.[rarity];
|
||||
|
||||
if(!board?.totalPlayers) continue;
|
||||
|
||||
const assets = RARITY_ASSETS[rarity];
|
||||
const rarityText = LocalizeText(`badge.rarity.${ rarity }`);
|
||||
|
||||
built.push({
|
||||
key: `rarity-${ rarity }`,
|
||||
rarity,
|
||||
board,
|
||||
frame: assets.frame,
|
||||
emblem: assets.emblem,
|
||||
title: () => LocalizeText('badge_leaderboard.title.rarity', [ 'rarity' ], [ rarityText ]),
|
||||
info: () => LocalizeText(`badge_leaderboard.info.rarity.${ rarity }`),
|
||||
option: () => LocalizeText('badge_leaderboard.option.rarity', [ 'rarity' ], [ rarityText ])
|
||||
});
|
||||
}
|
||||
|
||||
return built;
|
||||
}, [ leaderboard ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!pages.length) return;
|
||||
if(categoryIndex < pages.length) return;
|
||||
|
||||
setCategoryIndex(0);
|
||||
}, [ categoryIndex, pages.length ]);
|
||||
|
||||
useEffect(() => setEntryPageIndex(0), [ categoryIndex ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isCategoryMenuVisible) return;
|
||||
|
||||
const onWindowPointerDown = () => setIsCategoryMenuVisible(false);
|
||||
|
||||
window.addEventListener('pointerdown', onWindowPointerDown);
|
||||
|
||||
return () => window.removeEventListener('pointerdown', onWindowPointerDown);
|
||||
}, [ isCategoryMenuVisible ]);
|
||||
|
||||
const currentPage = pages[categoryIndex] || null;
|
||||
const allEntries = currentPage?.board?.entries || [];
|
||||
const viewerEntry = useMemo(() =>
|
||||
{
|
||||
const fromBoard = currentPage?.board?.viewerEntry as BadgeLeaderboardEntry;
|
||||
|
||||
if(fromBoard?.userId) return fromBoard;
|
||||
if(!leaderboard?.viewerUserId) return null;
|
||||
|
||||
const session = GetSessionDataManager();
|
||||
|
||||
if(!session || session.userId !== leaderboard.viewerUserId) return null;
|
||||
|
||||
return {
|
||||
userId: session.userId,
|
||||
username: session.userName || '',
|
||||
figure: session.figure || '',
|
||||
score: 0,
|
||||
rank: 0
|
||||
} as BadgeLeaderboardEntry;
|
||||
}, [ currentPage?.board?.viewerEntry, leaderboard?.viewerUserId ]);
|
||||
const rankedEntries = useMemo(() =>
|
||||
{
|
||||
if(!allEntries.length) return [];
|
||||
if(!viewerEntry?.userId) return allEntries;
|
||||
|
||||
return allEntries.filter(entry => entry.userId !== viewerEntry.userId);
|
||||
}, [ allEntries, viewerEntry?.userId ]);
|
||||
const viewerHasRankedScore = ((viewerEntry?.rank || 0) > 0);
|
||||
const rankedTotalPlayers = Math.max((currentPage?.board?.totalPlayers || 0) - (viewerHasRankedScore ? 1 : 0), rankedEntries.length);
|
||||
const totalEntryPages = Math.max(1, Math.ceil(rankedTotalPlayers / PAGE_SIZE));
|
||||
const clampedEntryPageIndex = Math.min(entryPageIndex, totalEntryPages - 1);
|
||||
const pageStart = clampedEntryPageIndex * PAGE_SIZE;
|
||||
const pageEntries = rankedEntries.slice(pageStart, pageStart + PAGE_SIZE);
|
||||
const showViewerEntry = !!viewerEntry?.userId;
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="nitro-badge-leaderboard fixed inset-0 z-[100] flex items-center justify-center pointer-events-none">
|
||||
<DraggableWindow uniqueKey="badge-leaderboard" handleSelector=".nitro-badge-leaderboard__drag-handle" windowPosition={ DraggableWindowPosition.CENTER }>
|
||||
<div className="nitro-badge-leaderboard__window pointer-events-auto" style={ { '--badge-leaderboard-frame': `url(${ currentPage?.frame || frameLeaderboardTotal })` } as CSSProperties }>
|
||||
<div className="nitro-badge-leaderboard__frame" aria-hidden="true" />
|
||||
<div className="nitro-badge-leaderboard__drag-handle" />
|
||||
<button className="nitro-badge-leaderboard__close" type="button" onPointerDown={ event => event.stopPropagation() } onClick={ () => setIsVisible(false) } aria-label="Close">
|
||||
<span className="nitro-badge-leaderboard__close-icon" style={ { backgroundImage: `url(${ leaderboardButtonCloseSwf })` } } />
|
||||
</button>
|
||||
<div className="nitro-badge-leaderboard__header">
|
||||
<button className="nitro-badge-leaderboard__category-button" type="button" onPointerDown={ event => event.stopPropagation() } onClick={ () => setIsCategoryMenuVisible(value => !value) }>
|
||||
<Text className="nitro-badge-leaderboard__header-title">{ currentPage?.title() || LocalizeText('badge_leaderboard.title.total_badges') }</Text>
|
||||
<img className="nitro-badge-leaderboard__header-arrow" src={ leaderboardDropdownOpener } alt="" />
|
||||
</button>
|
||||
{ isCategoryMenuVisible &&
|
||||
<div className="nitro-badge-leaderboard__category-menu" onPointerDown={ event => event.stopPropagation() }>
|
||||
{ pages.map((page, index) => (
|
||||
<button key={ page.key } className={ `nitro-badge-leaderboard__category-option ${ index === categoryIndex ? 'is-active' : '' }` } type="button" onClick={ () => { setCategoryIndex(index); setIsCategoryMenuVisible(false); } }>
|
||||
{ page.option() }
|
||||
</button>
|
||||
)) }
|
||||
</div> }
|
||||
</div>
|
||||
<div className="nitro-badge-leaderboard__content">
|
||||
{ isLoading && !leaderboard &&
|
||||
<div className="nitro-badge-leaderboard__state">
|
||||
{ LocalizeText('generic.loading') }
|
||||
</div> }
|
||||
{ loadError && !leaderboard &&
|
||||
<div className="nitro-badge-leaderboard__state nitro-badge-leaderboard__state--error">
|
||||
{ loadError }
|
||||
</div> }
|
||||
{ currentPage &&
|
||||
<>
|
||||
<div className="nitro-badge-leaderboard__info-card">
|
||||
<img className="nitro-badge-leaderboard__info-icon" src={ currentPage.emblem } alt="" />
|
||||
<Text className="nitro-badge-leaderboard__info-text" small wrap>{ currentPage.info() }</Text>
|
||||
</div>
|
||||
<div className="nitro-badge-leaderboard__list">
|
||||
{ pageEntries.map((entry, index) => <LeaderboardRow key={ `${ currentPage.key }-${ entry.userId }` } entry={ entry } emblem={ currentPage.emblem } rowIndex={ pageStart + index } isCurrentUser={ false } />) }
|
||||
{ showViewerEntry && <LeaderboardRow entry={ viewerEntry } emblem={ currentPage.emblem } rowIndex={ pageEntries.length } isCurrentUser={ true } />}
|
||||
</div>
|
||||
<img className="nitro-badge-leaderboard__divider" src={ leaderboardDivider } alt="" />
|
||||
<Flex className="nitro-badge-leaderboard__footer" justifyContent="between" alignItems="center">
|
||||
<button className="nitro-badge-leaderboard__nav-button is-previous" disabled={ clampedEntryPageIndex <= 0 } onClick={ () => setEntryPageIndex(value => Math.max(0, value - 1)) }>
|
||||
{ LocalizeText('badge_leaderboard.previous') }
|
||||
</button>
|
||||
<Column gap={ 0 } alignItems="center">
|
||||
<Text small bold>{ currentPage.option() }</Text>
|
||||
<Text className="opacity-70" small>{ `${ clampedEntryPageIndex + 1 } / ${ totalEntryPages }` }</Text>
|
||||
</Column>
|
||||
<button className="nitro-badge-leaderboard__nav-button is-next" disabled={ clampedEntryPageIndex >= (totalEntryPages - 1) } onClick={ () => setEntryPageIndex(value => Math.min(totalEntryPages - 1, value + 1)) }>
|
||||
{ LocalizeText('badge_leaderboard.next') }
|
||||
</button>
|
||||
</Flex>
|
||||
</> }
|
||||
</div>
|
||||
</div>
|
||||
</DraggableWindow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface LeaderboardRowProps
|
||||
{
|
||||
entry: BadgeLeaderboardEntry;
|
||||
emblem: string;
|
||||
rowIndex?: number;
|
||||
isCurrentUser?: boolean;
|
||||
}
|
||||
|
||||
const LeaderboardRow: FC<LeaderboardRowProps> = props =>
|
||||
{
|
||||
const { entry = null, emblem = null, rowIndex = 0, isCurrentUser = false } = props;
|
||||
|
||||
if(!entry) return null;
|
||||
|
||||
const rankClassName = ((entry.rank === 1) ? 'is-rank-1' : ((entry.rank === 2) ? 'is-rank-2' : ((entry.rank === 3) ? 'is-rank-3' : '')));
|
||||
|
||||
return (
|
||||
<div className={ `nitro-badge-leaderboard__row ${ isCurrentUser ? 'is-current-user' : '' } ${ ((rowIndex % 2) === 0) ? 'is-even' : 'is-odd' }` }>
|
||||
<div className={ `nitro-badge-leaderboard__rank ${ rankClassName }` }>{ entry.rank }</div>
|
||||
<div className="nitro-badge-leaderboard__avatar">
|
||||
<img className="nitro-badge-leaderboard__avatar-image" src={ getAvatarHeadUrl(entry.figure) } alt="" loading="lazy" />
|
||||
</div>
|
||||
<Text className="nitro-badge-leaderboard__username" bold>{ entry.username }</Text>
|
||||
<Text className="nitro-badge-leaderboard__score" bold>{ entry.score }</Text>
|
||||
<img className="nitro-badge-leaderboard__row-emblem" src={ emblem } alt="" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -205,7 +205,7 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
|
||||
onClick={ handleSlotClick }
|
||||
onDoubleClick={ handleDoubleClick }>
|
||||
{ badgeCode
|
||||
? <LayoutBadgeImageView badgeCode={ badgeCode } showInfo={ true } />
|
||||
? <LayoutBadgeImageView badgeCode={ badgeCode } showInfo={ true } showRarityInfo={ true } highlightRarity={ true } />
|
||||
: isOwnUser && <FaPlus className="text-white/30 text-[10px]" /> }
|
||||
</div>
|
||||
{ showPicker && (
|
||||
|
||||
@@ -61,6 +61,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
|
||||
case 'customize_nick':
|
||||
CreateLinkEvent('customize/show');
|
||||
break;
|
||||
case 'badge_leaderboard':
|
||||
CreateLinkEvent('badge-leaderboard/show');
|
||||
break;
|
||||
case 'expressions':
|
||||
hideMenu = false;
|
||||
setMode(MODE_EXPRESSIONS);
|
||||
@@ -149,6 +152,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
|
||||
<ContextMenuListItemView onClick={ event => processAction('customize_nick') }>
|
||||
Nick Custom
|
||||
</ContextMenuListItemView>
|
||||
<ContextMenuListItemView onClick={ event => processAction('badge_leaderboard') }>
|
||||
{ LocalizeText('badge_leaderboard.title.total_badges') }
|
||||
</ContextMenuListItemView>
|
||||
{ (HasHabboClub() && !isRidingHorse) &&
|
||||
<ContextMenuListItemView onClick={ event => processAction('dance_menu') }>
|
||||
<FaChevronRight className="right fa-icon" />
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
.nitro-badge-leaderboard__window {
|
||||
position: relative;
|
||||
width: 460px;
|
||||
height: 679px;
|
||||
overflow: hidden;
|
||||
image-rendering: pixelated;
|
||||
filter: drop-shadow(0 18px 32px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__drag-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 90px;
|
||||
z-index: 1;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-style: solid;
|
||||
border-width: 87px 96px 42px 96px;
|
||||
border-image-source: var(--badge-leaderboard-frame);
|
||||
border-image-slice: 87 96 42 96;
|
||||
border-image-width: 87px 96px 42px 96px;
|
||||
border-image-repeat: round;
|
||||
background: #ece8dc;
|
||||
background-clip: padding-box;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__frame::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 86px 95px 41px 95px;
|
||||
background: #ece8dc;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 108px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__close-icon {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__header {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__category-button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
min-width: 120px;
|
||||
max-width: 220px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__header-title {
|
||||
color: #ffffff;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-shadow:
|
||||
0 1px 0 #4a4a4a,
|
||||
1px 0 0 #4a4a4a,
|
||||
-1px 0 0 #4a4a4a,
|
||||
0 -1px 0 #4a4a4a;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__header-arrow {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-top: 3px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__category-menu {
|
||||
position: absolute;
|
||||
top: calc(100% - 2px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 154px;
|
||||
padding: 2px;
|
||||
border: 1px solid #8f8f8f;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__category-option {
|
||||
border: 0;
|
||||
padding: 5px 9px;
|
||||
background: transparent;
|
||||
color: #232323;
|
||||
font-size: 14px;
|
||||
line-height: 1.15;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__category-option:hover,
|
||||
.nitro-badge-leaderboard__category-option.is-active {
|
||||
background: rgba(117, 170, 207, 0.28);
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__content {
|
||||
position: absolute;
|
||||
inset: 80px 38px 30px 38px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: #1f1f1f;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #3d3d3d;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__state--error {
|
||||
color: #9f2b2b;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 52px;
|
||||
padding: 8px 12px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(231, 229, 221, 0.96));
|
||||
border: 1px solid #a4a094;
|
||||
border-radius: 10px;
|
||||
box-shadow: inset 0 1px 0 #ffffff;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__info-icon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 25px;
|
||||
max-height: 25px;
|
||||
image-rendering: pixelated;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__info-text {
|
||||
color: #3b3b3b;
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__row {
|
||||
display: grid;
|
||||
grid-template-columns: 34px 54px minmax(0, 1fr) 56px 28px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 36px;
|
||||
padding: 0 10px 0 8px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__row.is-even {
|
||||
background-image: url('../../assets/images/leaderboard_badge/leaderboard_entry_even.png');
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__row.is-odd {
|
||||
background-image: url('../../assets/images/leaderboard_badge/leaderboard_entry_uneven.png');
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__row.is-current-user {
|
||||
min-height: 40px;
|
||||
background-image: url('../../assets/images/leaderboard_badge/leaderboard_entry_self.png');
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__rank {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
background: linear-gradient(180deg, #6a8db7 0%, #4d6788 100%);
|
||||
border: 1px solid #546b87;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 0 #31455d;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__rank.is-rank-1 {
|
||||
background: linear-gradient(180deg, #efcf58 0%, #c28e1f 100%);
|
||||
border-color: #af8325;
|
||||
text-shadow: 0 1px 0 #7f6020;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__rank.is-rank-2 {
|
||||
background: linear-gradient(180deg, #d4d4d4 0%, #9d9d9d 100%);
|
||||
border-color: #8e8e8e;
|
||||
text-shadow: 0 1px 0 #666666;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__rank.is-rank-3 {
|
||||
background: linear-gradient(180deg, #db9c4f 0%, #a56521 100%);
|
||||
border-color: #9a5c1c;
|
||||
text-shadow: 0 1px 0 #784312;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__avatar {
|
||||
width: 54px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__avatar .avatar-image,
|
||||
.nitro-badge-leaderboard__avatar-image {
|
||||
display: block;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 54px;
|
||||
max-height: 62px;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
margin: 0 auto;
|
||||
image-rendering: pixelated;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__username {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #202020;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__score {
|
||||
width: 56px;
|
||||
color: #1f1f1f;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
text-align: right;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__row-emblem {
|
||||
display: block;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 25px;
|
||||
max-height: 25px;
|
||||
margin: 0 auto;
|
||||
image-rendering: pixelated;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__divider {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 2px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__footer {
|
||||
gap: 10px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__nav-button {
|
||||
min-width: 100px;
|
||||
height: 26px;
|
||||
border: 1px solid #8a8a8a;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, #fefefe 0%, #e8e8e6 100%);
|
||||
color: #1f1f1f;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__nav-button:hover:not(:disabled) {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #ece8dc 100%);
|
||||
}
|
||||
|
||||
.nitro-badge-leaderboard__nav-button:disabled {
|
||||
color: #8d8d8d;
|
||||
background: linear-gradient(180deg, #ececec 0%, #d9d9d9 100%);
|
||||
cursor: default;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { App } from './App';
|
||||
import './css/index.css';
|
||||
|
||||
import './css/backgrounds/BackgroundsView.css';
|
||||
import './css/badges/BadgeLeaderboardView.css';
|
||||
|
||||
import './css/chat/Chats.css';
|
||||
|
||||
|
||||