ui/Panels/LeaderboardPanel.razor

UI Razor panel that displays a leaderboard. It loads a Leaderboards.Board2 for a given difficulty, refreshes entries asynchronously, and renders rank, avatar, name and formatted time, with special handling for survival vs default filtering and loading/error states.

NetworkingFile Access
@using Sandbox;
@using Sandbox.UI;
@namespace SS1
@inherits Panel
@attribute [StyleSheet]

<root>
	<label class="header">@LabelText</label>

	@if (_isLoading)
	{
		<label class="status">Loading leaderboard…</label>
		return;
	}
	@if (_loadError)
	{
		<label class="status">Couldn’t load leaderboard.</label>
		return;
	}
	@if (Leaderboard is null || !Leaderboard.Entries.Any(entry => IsSurvival || entry.Value > (10 * 60 - 115)))
	{
		<label class="status">No scores available.</label>
		return;
	}

    <div class="entries">
		@{
			var offset = 0;
		}
        
        @foreach (var entry in Leaderboard.Entries)
        {
			@if(!IsSurvival && entry.Value <= (10 * 60 - 115))
			{
                offset++;
                continue;
            }

			<div class="entry" style="color:@(Color.Lerp(Color.White, new Color(0.3f, 0.3f, 0.3f), Utils.Map(entry.Rank - offset, 0, 40, 0f, 1f, EasingType.QuadOut)).Rgba);">
				<label class="rank">##@(entry.Rank - offset)</label>
				<div class="avatar" style="background-image: url( avatar:@entry.SteamId )"></div>
				<label class="name">@entry.DisplayName</label>
				<label class="value">@FormatTime(entry.Value)</label>
            </div>
        }
    </div>
</root>

@code
{
	public int Difficulty { get; set; }
	private int _lastDifficulty;

	public string LabelText { get; set; }

	public bool IsSurvival { get; set; }

	Sandbox.Services.Leaderboards.Board2 Leaderboard;
	bool _isLoading = true;
	bool _loadError;

	protected override void OnAfterTreeRender(bool firstTime)
	{
		base.OnAfterTreeRender(firstTime);

		if(firstTime || Difficulty != _lastDifficulty)
		{
			// if(IsSurvival)
			// {
			// 	Leaderboard = Sandbox.Services.Leaderboards.GetFromStat(Manager.GetStatName(Difficulty, survival: true));
			// 	Leaderboard.SetAggregationMax();
			// 	Leaderboard.SetSortDescending();
			// }
			// else
			// {
				Leaderboard = Sandbox.Services.Leaderboards.GetFromStat(Manager.GetStatName(Difficulty));
				Leaderboard.SetAggregationMin();
				Leaderboard.SetSortAscending();
			// }

			// Leaderboard = Sandbox.Services.Leaderboards.Get(LeaderboardName);
			_ = Refresh(Leaderboard);

			_lastDifficulty = Difficulty;
			// _lastLeaderboardName = LeaderboardName;
		}
    }

    async System.Threading.Tasks.Task Refresh(Sandbox.Services.Leaderboards.Board2 board)
    {
        _isLoading = true;
        _loadError = false;
        StateHasChanged();
        try
        {
            board.MaxEntries = 150;
            await board.Refresh();
        }
        catch (Exception exception)
        {
            Log.Warning($"Leaderboard request failed for {board.Stat}: {exception.Message}");
            if (ReferenceEquals(board, Leaderboard))
                _loadError = true;
        }
        finally
        {
            if (ReferenceEquals(board, Leaderboard))
            {
                _isLoading = false;
                StateHasChanged();
            }
        }
    }

	// protected override int BuildHash()
	// {
	// 	return HashCode.Combine(
	// 		LeaderboardName
	// 	);
	// }

	public static string FormatTime(double seconds)
	{
		// Convert the seconds to minutes and remaining seconds
		int minutes = (int)seconds / 60;
		int remainingSeconds = (int)seconds % 60;

		// Format the output as "MM:SS" with leading zeros if needed
		return $"{minutes}:{remainingSeconds:D2}";
	}
}