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