UI/LeaderboardPanel.razor

A Blazor/Sandbox.UI Razor component that renders a leaderboard card. It shows a title, filter tabs, an optional pinned "YOU" row, and a list of ranked ScoreEntry rows with avatars, country codes, ranks and values, trimmed to MaxRows.

@using System
@using System.Linq
@using Sandbox
@using Sandbox.UI
@using Sandbox.Services
@namespace Breakout
@inherits Panel

<root>
    <div class="sb-head">
        <div class="star">★</div>
        <div class="sb-title">@Title</div>
    </div>

    <div class="tabs">
        @foreach ( var tab in Tabs )
        {
            <button class="tab @(ActiveFilter == tab.Filter ? "active" : "")" style="@(ActiveFilter == tab.Filter ? "background-color: #4A9BFF; color: #ffffff;" : "background-color: #F6ECD4; color: #2B2440;")" onclick=@(() => FilterChanged?.Invoke( tab.Filter ))>@tab.Label</button>
        }
    </div>

    @if ( Me is { } meEntry )
    {
        <div class="row you">
            <div class="medal" style="background-color: @MedalColor( meEntry.Rank );">@meEntry.Rank</div>
            <div class="avatar" style="background-image: url( avatar:@meEntry.SteamId );"></div>
            <div class="name">YOU</div>
            <div class="right">
                @if ( !string.IsNullOrEmpty( Country( meEntry ) ) )
                {
                    <span class="country">@Country( meEntry )</span>
                }
                <span class="val">@meEntry.Value.ToString( "N0" )</span>
            </div>
        </div>
    }

    <div class="sb-list">
        @if ( Rows.Length == 0 )
        {
            <div class="board-empty">@(Loading ? "Loading…" : EmptyText())</div>
        }
        else
        {
            @foreach ( var e in Rows )
            {
                <div class="row @(e.SteamId == MySteamId ? "me" : "")">
                    <div class="medal" style="background-color: @MedalColor( e.Rank );">@e.Rank</div>
                    <div class="avatar" style="background-image: url( avatar:@e.SteamId );"></div>
                    <div class="name">@DisplayName( e )</div>
                    <div class="right">
                        @if ( !string.IsNullOrEmpty( Country( e ) ) )
                        {
                            <span class="country">@Country( e )</span>
                        }
                        <span class="val">@e.Value.ToString( "N0" )</span>
                    </div>
                </div>
            }
        }
    </div>
</root>

@code
{
    /// <summary>
    /// The ranked rows to show.
    /// </summary>
    [Parameter] public ScoreEntry[] Entries { get; set; }

    /// <summary>
    /// The local player's own row, pinned at the top (null hides it).
    /// </summary>
    [Parameter] public ScoreEntry? Me { get; set; }

    /// <summary>
    /// True while the parent is fetching, so the list shows a loading message.
    /// </summary>
    [Parameter] public bool Loading { get; set; }

    /// <summary>
    /// Which filter tab is currently selected.
    /// </summary>
    [Parameter] public ScoreFilter ActiveFilter { get; set; } = ScoreFilter.Global;

    /// <summary>
    /// Called with the new filter when the player clicks a tab.
    /// </summary>
    [Parameter] public Action<ScoreFilter> FilterChanged { get; set; }

    /// <summary>
    /// Heading text shown at the top of the card.
    /// </summary>
    [Parameter] public string Title { get; set; } = "LEADERBOARD";

    /// <summary>
    /// How many rows to show at most (0 means no limit). The pinned YOU row counts against this.
    /// </summary>
    [Parameter] public int MaxRows { get; set; }

    /// <summary>
    /// Bumped by the parent to force a re-render when the data changes.
    /// </summary>
    [Parameter] public int Version { get; set; }

    private (ScoreFilter Filter, string Label)[] Tabs => new[]
    {
        (ScoreFilter.Global, "GLOBAL"),
        (ScoreFilter.Friends, "FRIENDS"),
        (ScoreFilter.Country, "COUNTRY"),
        (ScoreFilter.AroundMe, "AROUND ME")
    };

    // The list rows to show, trimmed to fit MaxRows. When a YOU row is pinned above the list we
    // reserve one slot for it and drop the local player from the list, so they never appear twice.
    private ScoreEntry[] Rows
    {
        get
        {
            var all = Entries ?? Array.Empty<ScoreEntry>();

            var budget = MaxRows;
            if ( Me is not null )
            {
                all = all.Where( e => e.SteamId != MySteamId ).ToArray();
                if ( budget > 0 )
                    budget -= 1;
            }

            if ( budget > 0 && all.Length > budget )
                return all.Take( budget ).ToArray();

            return all;
        }
    }

    private long MySteamId => Sandbox.Utility.Steam.SteamId;

    private static string MedalColor( long rank ) => rank switch
    {
        1 => "#FFC93C",
        2 => "#4A9BFF",
        3 => "#46D07E",
        _ => "#ffffff"
    };

    private static string DisplayName( ScoreEntry e )
        => string.IsNullOrEmpty( e.DisplayName ) ? "Player" : e.DisplayName;

    private static string Country( ScoreEntry e )
        => string.IsNullOrEmpty( e.CountryCode ) ? "" : e.CountryCode.ToUpperInvariant();

    private string EmptyText() => ActiveFilter switch
    {
        ScoreFilter.Friends => "No friends ranked yet",
        ScoreFilter.Country => "No scores from your country yet",
        ScoreFilter.AroundMe => "You're not ranked yet",
        _ => "No scores yet"
    };

    protected override int BuildHash() => System.HashCode.Combine( Version, (int)ActiveFilter, Loading, Entries?.Length ?? -1, MaxRows, Me?.Rank ?? -1 );
}