From 32f9f7b0214dca5a82191242cdef18885e975f06 Mon Sep 17 00:00:00 2001
From: Luke Parker <10430890+Hona@users.noreply.github.com>
Date: Thu, 4 Jul 2024 18:46:34 +1000
Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Basic=20Home=20page=20+=20Top=20?=
=?UTF-8?q?Players=20Online=20(naive)=20(#61)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 💄 Basic Home page + Top Players Online (naive)
* persist prerendered state
---
.../{HomePage.razor => Index/IndexPage.razor} | 16 +-
.../Pages/Index/IndexPage.razor.css | 3 +
.../Pages/Index/PopularServersWidget.razor | 90 ++++++++++
.../Pages/Index/RecentRecordsWidget.razor | 63 +++++++
.../Leaderboards/Activity/ActivityPage.razor | 2 +
.../Pages/Play/TopPlayersOnlinePage.razor | 166 +++++++++++++++++-
.../Pages/Play/TopPlayersOnlinePage.razor.css | 13 ++
.../TF2Jump.WebUI.Client/Program.cs | 2 +
.../Services/ServerResolver.cs | 69 ++++++++
UI/src/TF2Jump.WebUI/TF2Jump.WebUI/Program.cs | 4 +-
10 files changed, 419 insertions(+), 9 deletions(-)
rename UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/{HomePage.razor => Index/IndexPage.razor} (61%)
create mode 100644 UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/IndexPage.razor.css
create mode 100644 UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/PopularServersWidget.razor
create mode 100644 UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/RecentRecordsWidget.razor
create mode 100644 UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Leaderboards/Activity/ActivityPage.razor
create mode 100644 UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Play/TopPlayersOnlinePage.razor.css
create mode 100644 UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Services/ServerResolver.cs
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/HomePage.razor b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/IndexPage.razor
similarity index 61%
rename from UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/HomePage.razor
rename to UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/IndexPage.razor
index 51f2a32..3d6dfd1 100644
--- a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/HomePage.razor
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/IndexPage.razor
@@ -5,9 +5,13 @@
-
- TF2 Jump is currently in development, and is not fully functional.
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/IndexPage.razor.css b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/IndexPage.razor.css
new file mode 100644
index 0000000..492e2b0
--- /dev/null
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/IndexPage.razor.css
@@ -0,0 +1,3 @@
+::deep .fluent-messagebar.intent-custom {
+ animation: none !important;
+}
\ No newline at end of file
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/PopularServersWidget.razor b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/PopularServersWidget.razor
new file mode 100644
index 0000000..85a1efc
--- /dev/null
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/PopularServersWidget.razor
@@ -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
+
+
+
+
+ Popular Servers
+
+
+
+ More
+
+
+ @if (_filteredServers is not null)
+ {
+
+ @foreach (var server in _filteredServers)
+ {
+
+
+ @server.GameInfo.PlayerCount/@server.GameInfo.MaxPlayers online on @server.GameInfo.CurrentMap
+
+
+ Join
+
+ }
+
+ }
+
+
+@code {
+ private List? _servers;
+ private List? _filteredServers;
+ private Dictionary _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>(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>(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();
+ }
+}
\ No newline at end of file
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/RecentRecordsWidget.razor b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/RecentRecordsWidget.razor
new file mode 100644
index 0000000..8ca048e
--- /dev/null
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Index/RecentRecordsWidget.razor
@@ -0,0 +1,63 @@
+@using Humanizer
+@using TempusApi.Models.Responses
+
+@implements IDisposable
+
+
+
+
+ Recent Records
+
+
+ More
+
+
+
+ @if (_activity is not null)
+ {
+
+ @foreach (var wr in _activity.MapRecords.Take(7))
+ {
+
+ @wr.PlayerInfo.Name broke @wr.MapInfo.Name @wr.RecordInfo.Date.ToDateTimeOffset().Humanize()
+
+ WR @wr.RecordInfo.Duration.ToTimeSpan().ToFormattedDuration()
+
+
+ }
+
+ }
+
+
+@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(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();
+ }
+}
\ No newline at end of file
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Leaderboards/Activity/ActivityPage.razor b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Leaderboards/Activity/ActivityPage.razor
new file mode 100644
index 0000000..6ef47ca
--- /dev/null
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Leaderboards/Activity/ActivityPage.razor
@@ -0,0 +1,2 @@
+@page "/leaderboards/activity"
+
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Play/TopPlayersOnlinePage.razor b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Play/TopPlayersOnlinePage.razor
index b5d0e93..51c6e34 100644
--- a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Play/TopPlayersOnlinePage.razor
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Play/TopPlayersOnlinePage.razor
@@ -1 +1,165 @@
-@page "/play/top-players-online"
\ No newline at end of file
+@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
+
+Top Players Online
+
+
+
+
+
+
+ @foreach(var topPlayer in _topPlayersOnline ?? [])
+ {
+
+
+
+
+
+ @(topPlayer.RealName ?? topPlayer.SteamName)
+
+
+
+
+ Join
+
+
+
+
+
+
+
+ Rank @topPlayer.Rank on @topPlayer.ServerInfo.CurrentMap
+
+
+
+
+ @topPlayer.ServerInfo.Alias
+
+
+
+
+
+ }
+
+
+
+@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 _steamProfilePictures = [];
+ private Dictionary _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(nameof(_topPlayersOnline), out var restored)
+ ? restored
+ : await HttpClient.GetFromJsonAsync("https://tempushub.xyz/api/TopPlayersOnline");
+
+ _steamProfilePictures = (ApplicationState.TryTakeFromJson>(nameof(_steamProfilePictures), out var pictures)
+ ? pictures : await HydrateSteamProfilePictures()) ?? throw new InvalidOperationException();
+
+ _dnsLookups = (ApplicationState.TryTakeFromJson>(nameof(_dnsLookups), out var lookups)
+ ? lookups : await HydrateDnsLookups()) ?? throw new InvalidOperationException();
+ }
+
+ private async Task> HydrateSteamProfilePictures()
+ {
+ if (_topPlayersOnline != null)
+ {
+ var tempusPlayerIds = _topPlayersOnline
+ .Select(x => x.TempusId)
+ .Where(x => x is not null)
+ .Cast();
+
+ _steamProfilePictures = await TempusClient.GetSteamProfilesAsync(tempusPlayerIds);
+ return _steamProfilePictures;
+ }
+
+ return [];
+ }
+
+ private async Task> 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
+ );
+}
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Play/TopPlayersOnlinePage.razor.css b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Play/TopPlayersOnlinePage.razor.css
new file mode 100644
index 0000000..b8ae014
--- /dev/null
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Components/Pages/Play/TopPlayersOnlinePage.razor.css
@@ -0,0 +1,13 @@
+::deep .fluent-persona {
+ width: 100%;
+}
+
+::deep .fluent-persona .name {
+ width: 100%;
+}
+
+::deep .top-player-card {
+
+ flex: 1 0 auto;
+
+}
\ No newline at end of file
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Program.cs b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Program.cs
index b107d3e..bf705d5 100644
--- a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Program.cs
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Program.cs
@@ -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);
@@ -13,5 +14,6 @@
builder.Services.AddHttpClient();
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
await builder.Build().RunAsync();
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Services/ServerResolver.cs b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Services/ServerResolver.cs
new file mode 100644
index 0000000..a62fcea
--- /dev/null
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI.Client/Services/ServerResolver.cs
@@ -0,0 +1,69 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json.Serialization;
+using TempusApi.Models;
+using TempusApi.Models.Responses;
+using TF2Jump.WebUI.Client.Components.Pages.Play;
+
+namespace TF2Jump.WebUI.Client.Services;
+
+public class ServerResolver
+{
+ private HttpClient _httpClient;
+
+ public ServerResolver(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ public string GetConnectUri(ServerStatusModel server, Dictionary dnsLookup) => GetConnectUri(server.ServerInfo, dnsLookup);
+
+ public string GetConnectUri(ServerInfo server, Dictionary dnsLookup)
+ {
+ if (dnsLookup.TryGetValue(server.Id, out var value))
+ {
+ return $"steam://connect/{value}:{server.Port}";
+ }
+
+ return $"steam://connect/{server.Addr}:{server.Port}";
+ }
+ public async Task> HydrateDnsLookups(List servers) => await HydrateDnsLookups(servers.Select(x => x.ServerInfo).ToList());
+
+ public async Task> HydrateDnsLookups(List servers)
+ {
+ var output = new Dictionary();
+
+ foreach (var server in servers)
+ {
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(server.Addr))
+ {
+ var response = await _httpClient.GetFromJsonAsync("https://dns.google/resolve?name=" + server.Addr);
+ var ip = response?.Answer?[0].Data;
+ if (ip != null) output[server.Id] = IPAddress.Parse(ip);
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine("Failed to resolve server IP");
+ Console.WriteLine(e);
+ }
+ }
+
+ return output;
+ }
+}
+
+public class DnsResult
+{
+ [JsonPropertyName("Answer")]
+ // ReSharper disable once MemberHidesStaticFromOuterClass
+ public Answer[] Answer { get; set; } = null!;
+}
+
+public class Answer
+{
+ [JsonPropertyName("data")]
+ public required string Data { get; set; }
+}
\ No newline at end of file
diff --git a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI/Program.cs b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI/Program.cs
index b3b6122..45465b2 100644
--- a/UI/src/TF2Jump.WebUI/TF2Jump.WebUI/Program.cs
+++ b/UI/src/TF2Jump.WebUI/TF2Jump.WebUI/Program.cs
@@ -3,6 +3,7 @@
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.FluentUI.AspNetCore.Components;
using TempusApi;
+using TF2Jump.WebUI.Client.Services;
using TF2Jump.WebUI.Components;
using TF2Jump.WebUI.Utilities.Humanizer;
@@ -12,10 +13,8 @@
// Add services to the container.
builder.Services.AddRazorComponents()
- .AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
-
builder.Services.AddFluentUIComponents();
if (builder.Environment.IsProduction())
@@ -32,6 +31,7 @@
builder.Services.AddHttpClient();
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
var app = builder.Build();