UI/LeaderboardPage.razor

A UI Razor component for the leaderboard page. It displays best lap times for a selected map, supports a friends-only filter, loads entries asynchronously from LapTimeService, and formats times for display.

NetworkingFile Access
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.UI.Navigation;
@using Sandbox.Services;
@using Machines.Components;
@using Machines.Resources;
@using Machines.Systems;
@using System.Linq;

@namespace Machines.UI
@inherits Panel
@attribute [Route( "/leaderboard" )]

@{
    var localSteamId = (long)Connection.Local.SteamId;
}

<root class="leaderboard-page @(Input.UsingController ? "controller" : "mouse")">
    <div class="eyebrow">BEST LAP TIMES</div>
    <div class="page-title">@(_map?.Title ?? "Leaderboard")</div>

    <div class="filter-row" onclick=@ToggleFriendsOnly>
        <div class="checkbox @(_friendsOnly ? "checked" : "")">
            @if ( _friendsOnly )
            {
                <span class="check-glyph">✓</span>
            }
        </div>
        <div class="filter-label">FRIENDS ONLY</div>
    </div>

    <div class="board">
        @if ( _loading )
        {
            <div class="board-msg">LOADING…</div>
        }
        else if ( _entries == null || _entries.Length == 0 )
        {
            <div class="board-msg">No lap times recorded yet.</div>
        }
        else
        {
            @foreach ( var entry in _entries )
            {
                <div class="row @(entry.SteamId == localSteamId ? "local" : "")">
                    <div class="row-pos">@entry.Rank</div>
                    <div class="row-pill">
                        <div class="row-name">@entry.DisplayName</div>
                        <div class="row-time">@FormatLap( (float)entry.Value )</div>
                    </div>
                </div>
            }
        }
    </div>

    <div class="footer-row">
        <div class="back-btn" onclick=@GoBack>
            <InputHint Action="MenuBack" Dark=@true class="back-glyph" />
            <span class="back-label">BACK</span>
        </div>
    </div>
</root>

@code
{
    private Leaderboards.Board2.Entry[] _entries;
    private bool _loading = true;
    private bool _friendsOnly;

    private MapResource _map;
    private string _mapPath;

    // Map comes from the ?map= query string, reapplied on every nav.
    public override void SetProperty( string name, string value )
    {
        base.SetProperty( name, value );

        if ( name == "map" )
        {
            _mapPath = value;
            _map = ResolveMap( value );
            _ = LoadAsync();
        }
    }

    private static MapResource ResolveMap( string resourcePath )
    {
        if ( string.IsNullOrEmpty( resourcePath ) )
            return null;

        // Try loaded pool first, fall back to Get.
        return ResourceLibrary.GetAll<MapResource>().FirstOrDefault( m => m.ResourcePath == resourcePath )
            ?? ResourceLibrary.Get<MapResource>( resourcePath );
    }

    private void ToggleFriendsOnly()
    {
        _friendsOnly = !_friendsOnly;
        _ = LoadAsync();
    }

    private async Task LoadAsync()
    {
        _loading = true;
        _entries = null;
        StateHasChanged();

        if ( !string.IsNullOrEmpty( _mapPath ) )
        {
            try
            {
                _entries = await LapTimeService.GetLeaderboard( _mapPath, 10, _friendsOnly );
            }
            catch ( Exception e )
            {
                Log.Warning( e, "LeaderboardPage: failed to fetch leaderboard" );
            }
        }

        _loading = false;
        StateHasChanged();
    }

    private void GoBack()
    {
        this.GetNavigator()?.GoBack();
    }

    public override void Tick()
    {
        base.Tick();

        if ( Input.Pressed( "MenuBack" ) )
            GoBack();
    }

    // Lap times use mm:ss.fff precision.
    private static string FormatLap( float seconds )
    {
        var mins = (int)(seconds / 60f);
        var secs = seconds - mins * 60f;
        return $"{mins:00}:{secs:00.000}";
    }

    protected override int BuildHash() =>
        System.HashCode.Combine( Input.UsingController, _loading, _entries?.Length ?? 0, _friendsOnly, _mapPath );
}