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 );
}