Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💄 Basic Home page + Top Players Online (naive) #61

Merged
merged 2 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading