Skip to content

Commit

Permalink
💄 Basic Home page + Top Players Online (naive) (#61)
Browse files Browse the repository at this point in the history
* 💄 Basic Home page + Top Players Online (naive)

* persist prerendered state
  • Loading branch information
Hona authored Jul 4, 2024
1 parent e82af29 commit 32f9f7b
Show file tree
Hide file tree
Showing 10 changed files with 419 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
<meta name="description" content="The premiere website for everything rocket jump and sticky jump related. This is a massive data driven site, utilising a complex ELT pipeline. Data sources include the Tempus2.xyz API, Tempus STV demo files (.dem), Tempus Archive on YouTube and much more."/>
</HeadContent>

<FluentMessageBar
Class="dark"
Intent="MessageIntent.Warning"
AllowDismiss="false">
TF2 Jump is currently in development, and is not fully functional.
</FluentMessageBar>
<div>
<FluentGrid>
<FluentGridItem>
<RecentRecordsWidget />
</FluentGridItem>
<FluentGridItem>
<PopularServersWidget />
</FluentGridItem>
</FluentGrid>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
::deep .fluent-messagebar.intent-custom {
animation: none !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
@using System.Net
@using System.Text.Json.Serialization
@using TempusApi.Models
@using TempusApi.Models.Responses
@using TF2Jump.WebUI.Client.Services

@implements IDisposable

<FluentCard>
<FluentStack>
<h3>
Popular Servers
</h3>
<FluentSpacer/>
<FluentAnchor Href="/play/servers"
IconEnd="@(new Icons.Regular.Size16.ArrowRight())">
More
</FluentAnchor>
</FluentStack>
@if (_filteredServers is not null)
{
<FluentStack Orientation="@Orientation.Vertical" VerticalGap="8">
@foreach (var server in _filteredServers)
{
<FluentMessageBar Title="@server.ServerInfo.Name"
Icon="@(new Icons.Regular.Size16.Server())"
Intent="MessageIntent.Custom"
AllowDismiss="false">
<FluentSpacer/>
@server.GameInfo.PlayerCount/@server.GameInfo.MaxPlayers online on @server.GameInfo.CurrentMap
<br/>

<a href="@ServerResolver.GetConnectUri(server, _dnsLookups)">Join</a>
</FluentMessageBar>
}
</FluentStack>
}
</FluentCard>

@code {
private List<ServerStatusModel>? _servers;
private List<ServerStatusModel>? _filteredServers;
private Dictionary<long, IPAddress> _dnsLookups = [];
private PersistingComponentStateSubscription _persistSubscription;

[Inject] public required ITempusClient TempusClient { get; set; }
[Inject] public required HttpClient HttpClient { get; set; }
[Inject] public required PersistentComponentState ApplicationState { get; set; }
[Inject] public required ServerResolver ServerResolver { get; set; }

protected override async Task OnInitializedAsync()
{
_persistSubscription = ApplicationState.RegisterOnPersisting(PersistData);

_servers = ApplicationState.TryTakeFromJson<List<ServerStatusModel>>(nameof(_servers), out var servers)
? servers : await TempusClient.GetServersStatusesAsync();

if (_servers == null)
{
throw new Exception("Failed to load server statuses");
}

_filteredServers = _servers.Where(x => x.GameInfo is not null)
.OrderByDescending(x => x.GameInfo.PlayerCount)
.Take(5)
.ToList();

_dnsLookups = (ApplicationState.TryTakeFromJson<Dictionary<long, IPAddress>>(nameof(_dnsLookups), out var lookups)
? lookups : []) ?? throw new InvalidOperationException();

if (lookups is null)
{
_dnsLookups = await ServerResolver.HydrateDnsLookups(_filteredServers);
}
}

private Task PersistData()
{
ApplicationState.PersistAsJson(nameof(_servers), _servers);
ApplicationState.PersistAsJson(nameof(_dnsLookups), _dnsLookups);

return Task.CompletedTask;
}


public void Dispose()
{
_persistSubscription.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@using Humanizer
@using TempusApi.Models.Responses

@implements IDisposable

<FluentCard>
<h3>
<FluentStack>
Recent Records
<FluentSpacer/>
<FluentAnchor Href="/leaderboards/activity"
IconEnd="@(new Icons.Regular.Size16.ArrowRight())">
More
</FluentAnchor>
</FluentStack>
</h3>
@if (_activity is not null)
{
<FluentStack Orientation="@Orientation.Vertical" VerticalGap="8">
@foreach (var wr in _activity.MapRecords.Take(7))
{
<FluentMessageBar Title="Map WR"
Icon="@(new Icons.Regular.Size16.Trophy())"
Intent="MessageIntent.Custom"
AllowDismiss="false">
@wr.PlayerInfo.Name broke @wr.MapInfo.Name @wr.RecordInfo.Date.ToDateTimeOffset().Humanize()
<br/>
WR @wr.RecordInfo.Duration.ToTimeSpan().ToFormattedDuration()

</FluentMessageBar>
}
</FluentStack>
}
</FluentCard>

@code {
[Inject] public required ITempusClient TempusClient { get; set; }
[Inject] public required PersistentComponentState ApplicationState { get; set; }

private RecentActivityModel? _activity;
private PersistingComponentStateSubscription _persistSubscription;

protected override async Task OnInitializedAsync()
{
_persistSubscription = ApplicationState.RegisterOnPersisting(PersistData);

_activity = ApplicationState.TryTakeFromJson<RecentActivityModel>(nameof(_activity), out var restored)
? restored
: await TempusClient.GetRecentActivityAsync();
}

private Task PersistData()
{
ApplicationState.PersistAsJson(nameof(_activity), _activity);

return Task.CompletedTask;
}

public void Dispose()
{
_persistSubscription.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@page "/leaderboards/activity"

Original file line number Diff line number Diff line change
@@ -1 +1,165 @@
@page "/play/top-players-online"
@page "/play/top-players-online"
@using System.Net
@using System.Text.Json.Serialization
@using TempusApi.Enums
@using TF2Jump.WebUI.Client.Services
@using TempusApi.Models

@implements IDisposable

<PageTitle>Top Players Online</PageTitle>
<HeadContent>
<meta name="description" content="Find players to spectate or chat with, that are currently online on Tempus"/>
</HeadContent>

<div>
<FluentStack Wrap
VerticalAlignment="VerticalAlignment.Center"
HorizontalAlignment="HorizontalAlignment.Start"
HorizontalGap="16"
VerticalGap="16">
@foreach(var topPlayer in _topPlayersOnline ?? [])
{
<FluentCard Width="fit-content" MinimalStyle Class="top-player-card">
<FluentStack >
<FluentStack HorizontalGap="8" VerticalAlignment="VerticalAlignment.Center" Orientation="Orientation.Vertical">
<FluentPersona Style="width: 100%"
Status="PresenceStatus.Available"
StatusSize="PresenceBadgeSize.Small"
Image="@(GetSteamProfilePicture(topPlayer.TempusId ?? 0))"
ImageSize="50px">
<FluentStack VerticalAlignment="VerticalAlignment.Center" HorizontalAlignment="HorizontalAlignment.Center">
@(topPlayer.RealName ?? topPlayer.SteamName)

<FluentSpacer/>

<a href="@ServerResolver.GetConnectUri(new TempusApi.Models.ServerInfo() { Id = topPlayer.ServerInfo.Id??0, Addr = topPlayer.ServerInfo.IpAddress.Split(":")[0], Port = int.Parse(topPlayer.ServerInfo.IpAddress.Split(":")[1])}, _dnsLookups)" style="margin-left: 16px">
Join
</a>
</FluentStack>


</FluentPersona>
<FluentStack VerticalAlignment="VerticalAlignment.Center">
<ClassIcon Color="white" Size="24" Class="@(@topPlayer.RankClass is 4 ? Class.Demoman : Class.Soldier)"/>
Rank @topPlayer.Rank on @topPlayer.ServerInfo.CurrentMap

</FluentStack>
<FluentStack VerticalAlignment="VerticalAlignment.Center">
<FluentIcon Value="@(new Icons.Filled.Size20.Server())" Color="Color.Neutral"/>
@topPlayer.ServerInfo.Alias
</FluentStack>
</FluentStack>

</FluentStack>
</FluentCard>
}
</FluentStack>
</div>

@code
{
private TopPlayerOnlineResult[]? _topPlayersOnline;
[Inject] public required HttpClient HttpClient { get; set; }
[Inject] public required ITempusClient TempusClient { get; set; }
[Inject] public required ServerResolver ServerResolver { get; set; }
[Inject] public required PersistentComponentState ApplicationState { get; set; }

private Dictionary<long, SteamProfile> _steamProfilePictures = [];
private Dictionary<long, IPAddress> _dnsLookups =[];
private PersistingComponentStateSubscription _persistSubscription;

private string GetSteamProfilePicture(long tempusId)
{
if (_steamProfilePictures.TryGetValue(tempusId, out var profile))
{
return profile.Avatars.LargeUrl;
}

return "";
}

protected override async Task OnInitializedAsync()
{
_persistSubscription = ApplicationState.RegisterOnPersisting(PersistData);

// TODO: Call our own API to get the top players online
// but API work is in a few weeks
_topPlayersOnline = ApplicationState.TryTakeFromJson<TopPlayerOnlineResult[]>(nameof(_topPlayersOnline), out var restored)
? restored
: await HttpClient.GetFromJsonAsync<TopPlayerOnlineResult[]>("https://tempushub.xyz/api/TopPlayersOnline");

_steamProfilePictures = (ApplicationState.TryTakeFromJson<Dictionary<long, SteamProfile>>(nameof(_steamProfilePictures), out var pictures)
? pictures : await HydrateSteamProfilePictures()) ?? throw new InvalidOperationException();

_dnsLookups = (ApplicationState.TryTakeFromJson<Dictionary<long, IPAddress>>(nameof(_dnsLookups), out var lookups)
? lookups : await HydrateDnsLookups()) ?? throw new InvalidOperationException();
}

private async Task<Dictionary<long, SteamProfile>> HydrateSteamProfilePictures()
{
if (_topPlayersOnline != null)
{
var tempusPlayerIds = _topPlayersOnline
.Select(x => x.TempusId)
.Where(x => x is not null)
.Cast<long>();

_steamProfilePictures = await TempusClient.GetSteamProfilesAsync(tempusPlayerIds);
return _steamProfilePictures;
}

return [];
}

private async Task<Dictionary<long, IPAddress>> HydrateDnsLookups()
{
var servers = (_topPlayersOnline ?? throw new InvalidOperationException())
.Select(x => new TempusApi.Models.ServerInfo()
{
Id = x.ServerInfo.Id??0,
Addr = x.ServerInfo.IpAddress.Split(":")[0],
Name = x.ServerInfo.Name,
Port = int.Parse(x.ServerInfo.IpAddress.Split(":")[1])
})
.DistinctBy(x => x.Id)
.ToList();

await ServerResolver.HydrateDnsLookups(servers);

return _dnsLookups;
}

private Task PersistData()
{
ApplicationState.PersistAsJson(nameof(_topPlayersOnline), _topPlayersOnline);
ApplicationState.PersistAsJson(nameof(_steamProfilePictures), _steamProfilePictures);
ApplicationState.PersistAsJson(nameof(_dnsLookups), _dnsLookups);

return Task.CompletedTask;
}

public void Dispose()
{
_persistSubscription.Dispose();
}

public record TopPlayerOnlineResult(
[property: JsonPropertyName("steamName")] string SteamName,
[property: JsonPropertyName("realName")] string RealName,
[property: JsonPropertyName("serverInfo")] ServerInfo ServerInfo,
[property: JsonPropertyName("tempusId")] long? TempusId,
[property: JsonPropertyName("rank")] int? Rank,
[property: JsonPropertyName("rankClass")] int? RankClass
);

public record ServerInfo(
[property: JsonPropertyName("alias")] string Alias,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("currentPlayers")] int? CurrentPlayers,
[property: JsonPropertyName("maxPlayers")] int? MaxPlayers,
[property: JsonPropertyName("currentMap")] string CurrentMap,
[property: JsonPropertyName("ipAddress")] string IpAddress,
[property: JsonPropertyName("id")] long? Id
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
::deep .fluent-persona {
width: 100%;
}

::deep .fluent-persona .name {
width: 100%;
}

::deep .top-player-card {

flex: 1 0 auto;

}
2 changes: 2 additions & 0 deletions UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.FluentUI.AspNetCore.Components;
using TempusApi;
using TF2Jump.WebUI.Client.Services;
using TF2Jump.WebUI.Utilities.Humanizer;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
Expand All @@ -13,5 +14,6 @@

builder.Services.AddHttpClient<ITempusClient, TempusClient>();
builder.Services.AddSingleton<ITempusClient, TempusClient>();
builder.Services.AddSingleton<ServerResolver>();

await builder.Build().RunAsync();
Loading

0 comments on commit 32f9f7b

Please sign in to comment.