Ajoute l Elo et les statistiques de parties
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Net
|
||||
@using System.Net.Http.Json
|
||||
@using ChessCubing.App.Models.Stats
|
||||
@using ChessCubing.App.Models.Users
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@@ -150,6 +151,116 @@
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="user-stats-block">
|
||||
<div class="section-heading user-profile-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Classement joueur</p>
|
||||
<h3>Elo et statistiques de partie</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(StatsLoadError))
|
||||
{
|
||||
<p class="profile-feedback error">@StatsLoadError</p>
|
||||
}
|
||||
else if (Stats is not null)
|
||||
{
|
||||
<div class="user-stats-grid">
|
||||
<article class="user-summary-card user-stat-emphasis">
|
||||
<span class="micro-label">Elo actuel</span>
|
||||
<strong>@Stats.CurrentElo</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Parties classees</span>
|
||||
<strong>@Stats.RankedGames</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Parties amicales</span>
|
||||
<strong>@Stats.CasualGames</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Victoires</span>
|
||||
<strong>@Stats.Wins</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Defaites</span>
|
||||
<strong>@Stats.Losses</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Parties stoppees</span>
|
||||
<strong>@Stats.StoppedGames</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Taux de victoire</span>
|
||||
<strong>@StatsWinRateLabel</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Coups joues</span>
|
||||
<strong>@Stats.TotalMoves</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Rounds cube</span>
|
||||
<strong>@Stats.TotalCubeRounds</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Meilleur cube</span>
|
||||
<strong>@FormatDurationShort(Stats.BestCubeTimeMs)</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Moyenne cube</span>
|
||||
<strong>@FormatDurationShort(Stats.AverageCubeTimeMs)</strong>
|
||||
</article>
|
||||
<article class="user-summary-card">
|
||||
<span class="micro-label">Derniere partie</span>
|
||||
<strong>@FormatOptionalDate(Stats.LastMatchUtc)</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="user-recent-matches">
|
||||
<div class="social-card-head">
|
||||
<strong>Dernieres parties</strong>
|
||||
<span class="mini-chip admin-chip-outline">@Stats.RecentMatches.Length match(s)</span>
|
||||
</div>
|
||||
|
||||
@if (Stats.RecentMatches.Length > 0)
|
||||
{
|
||||
<div class="social-list">
|
||||
@foreach (var match in Stats.RecentMatches)
|
||||
{
|
||||
<article class="social-item recent-match-item">
|
||||
<div class="social-item-meta">
|
||||
<div class="social-item-title-row">
|
||||
<strong>@BuildRecentMatchTitle(match)</strong>
|
||||
<span class="@BuildRecentMatchBadgeClass(match)">@BuildRecentMatchBadge(match)</span>
|
||||
</div>
|
||||
<span class="social-item-subtitle">@BuildRecentMatchSubtitle(match)</span>
|
||||
<span class="social-item-caption">@BuildRecentMatchCaption(match)</span>
|
||||
</div>
|
||||
|
||||
<div class="social-item-actions recent-match-meta">
|
||||
@if (match.EloDelta is not null)
|
||||
{
|
||||
<span class="@BuildEloDeltaClass(match.EloDelta.Value)">
|
||||
Elo @BuildSignedNumber(match.EloDelta.Value)
|
||||
</span>
|
||||
}
|
||||
|
||||
<span class="mini-chip admin-chip-neutral">
|
||||
@match.PlayerMoves coup(s)
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="social-empty">Aucune partie enregistree pour le moment.</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="callout user-profile-bio">
|
||||
<span class="micro-label">Bio</span>
|
||||
<p>@(Profile.Bio ?? "Aucune bio enregistree pour le moment.")</p>
|
||||
@@ -455,6 +566,7 @@
|
||||
private readonly UserProfileFormModel Form = new();
|
||||
|
||||
private UserProfileResponse? Profile;
|
||||
private UserStatsResponse? Stats;
|
||||
private SocialOverviewResponse? SocialOverview;
|
||||
private SocialSearchUserResponse[] SearchResults = [];
|
||||
private bool IsAuthenticated;
|
||||
@@ -464,6 +576,7 @@
|
||||
private bool IsSearching;
|
||||
private int _knownSocialVersion;
|
||||
private string? LoadError;
|
||||
private string? StatsLoadError;
|
||||
private string? SaveError;
|
||||
private string? SaveMessage;
|
||||
private string? SocialLoadError;
|
||||
@@ -486,6 +599,21 @@
|
||||
? "Le serveur verifie la fiche utilisateur et les relations sociales associees a ce compte."
|
||||
: $"Compte lie a {Profile?.Username ?? "l'utilisateur connecte"} avec synchronisation sociale en direct.";
|
||||
|
||||
private string StatsWinRateLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
var totalDecisiveGames = (Stats?.Wins ?? 0) + (Stats?.Losses ?? 0);
|
||||
if (totalDecisiveGames <= 0)
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
|
||||
var winRate = ((double)(Stats?.Wins ?? 0) / totalDecisiveGames) * 100d;
|
||||
return $"{Math.Round(winRate, MidpointRounding.AwayFromZero):0}%";
|
||||
}
|
||||
}
|
||||
|
||||
private string FriendCountLabel => $"{SocialOverview?.Friends.Length ?? 0} ami(s)";
|
||||
|
||||
private string ReceivedCountLabel => $"{SocialOverview?.ReceivedInvitations.Length ?? 0} recue(s)";
|
||||
@@ -561,6 +689,7 @@
|
||||
{
|
||||
LoadError = await ReadErrorAsync(response, "Le profil utilisateur n'a pas pu etre charge.");
|
||||
Profile = null;
|
||||
ResetStatsState();
|
||||
ResetSocialState();
|
||||
return;
|
||||
}
|
||||
@@ -569,23 +698,27 @@
|
||||
if (Profile is null)
|
||||
{
|
||||
LoadError = "Le serveur a retourne une reponse vide.";
|
||||
ResetStatsState();
|
||||
ResetSocialState();
|
||||
return;
|
||||
}
|
||||
|
||||
FillForm(Profile);
|
||||
await LoadStatsAsync();
|
||||
await LoadSocialOverviewAsync();
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
LoadError = "Le service utilisateur est temporairement indisponible.";
|
||||
Profile = null;
|
||||
ResetStatsState();
|
||||
ResetSocialState();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
LoadError = "La reponse du service utilisateur a pris trop de temps.";
|
||||
Profile = null;
|
||||
ResetStatsState();
|
||||
ResetSocialState();
|
||||
}
|
||||
finally
|
||||
@@ -595,6 +728,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadStatsAsync()
|
||||
{
|
||||
StatsLoadError = null;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.GetAsync("api/users/me/stats");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
StatsLoadError = await ReadErrorAsync(response, "Les statistiques joueur n'ont pas pu etre chargees.");
|
||||
Stats = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Stats = await response.Content.ReadFromJsonAsync<UserStatsResponse>();
|
||||
Stats ??= new UserStatsResponse();
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
StatsLoadError = "Le service de statistiques est temporairement indisponible.";
|
||||
Stats = null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
StatsLoadError = "Le chargement des statistiques a pris trop de temps.";
|
||||
Stats = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadSocialOverviewAsync()
|
||||
{
|
||||
SocialLoadError = null;
|
||||
@@ -849,9 +1011,16 @@
|
||||
{
|
||||
IsAuthenticated = false;
|
||||
Profile = null;
|
||||
ResetStatsState();
|
||||
Form.Reset();
|
||||
}
|
||||
|
||||
private void ResetStatsState()
|
||||
{
|
||||
Stats = null;
|
||||
StatsLoadError = null;
|
||||
}
|
||||
|
||||
private void ResetSocialState()
|
||||
{
|
||||
SocialOverview = null;
|
||||
@@ -920,6 +1089,83 @@
|
||||
private static string FormatDate(DateTime value)
|
||||
=> value.ToLocalTime().ToString("dd MMM yyyy 'a' HH:mm", CultureInfo.GetCultureInfo("fr-FR"));
|
||||
|
||||
private static string FormatOptionalDate(DateTime? value)
|
||||
=> value is null ? "--" : FormatDate(value.Value);
|
||||
|
||||
private static string FormatDurationShort(long? value)
|
||||
{
|
||||
if (value is null or <= 0)
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
|
||||
return MatchEngine.FormatStopwatch(value.Value);
|
||||
}
|
||||
|
||||
private static string BuildRecentMatchTitle(UserRecentMatchResponse match)
|
||||
=> string.IsNullOrWhiteSpace(match.MatchLabel)
|
||||
? $"{match.PlayerName} vs {match.OpponentName}"
|
||||
: match.MatchLabel;
|
||||
|
||||
private static string BuildRecentMatchSubtitle(UserRecentMatchResponse match)
|
||||
=> $"{BuildModeLabel(match.Mode)} • {match.OpponentName} • cote {BuildColorLabel(match.PlayerColor)}";
|
||||
|
||||
private static string BuildRecentMatchCaption(UserRecentMatchResponse match)
|
||||
{
|
||||
var parts = new List<string>
|
||||
{
|
||||
$"Le {FormatDate(match.CompletedUtc)}",
|
||||
$"{match.CubeRounds} round(s) cube",
|
||||
};
|
||||
|
||||
if (match.PlayerAverageCubeTimeMs is > 0)
|
||||
{
|
||||
parts.Add($"moy. cube {FormatDurationShort(match.PlayerAverageCubeTimeMs)}");
|
||||
}
|
||||
|
||||
return string.Join(" • ", parts);
|
||||
}
|
||||
|
||||
private static string BuildRecentMatchBadge(UserRecentMatchResponse match)
|
||||
{
|
||||
if (match.IsWin)
|
||||
{
|
||||
return "Victoire";
|
||||
}
|
||||
|
||||
if (match.IsLoss)
|
||||
{
|
||||
return "Defaite";
|
||||
}
|
||||
|
||||
return "Arretee";
|
||||
}
|
||||
|
||||
private static string BuildRecentMatchBadgeClass(UserRecentMatchResponse match)
|
||||
=> match.IsWin
|
||||
? "presence-badge online"
|
||||
: match.IsLoss
|
||||
? "presence-badge recent-loss-badge"
|
||||
: "presence-badge";
|
||||
|
||||
private static string BuildColorLabel(string color)
|
||||
=> string.Equals(color, MatchEngine.ColorWhite, StringComparison.OrdinalIgnoreCase)
|
||||
? "blanc"
|
||||
: "noir";
|
||||
|
||||
private static string BuildModeLabel(string mode)
|
||||
=> MatchEngine.Modes.TryGetValue(mode, out var info)
|
||||
? info.Label
|
||||
: mode;
|
||||
|
||||
private static string BuildSignedNumber(int value)
|
||||
=> value > 0 ? $"+{value}" : value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
private static string BuildEloDeltaClass(int value)
|
||||
=> value >= 0
|
||||
? "mini-chip user-elo-chip-positive"
|
||||
: "mini-chip user-elo-chip-negative";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
|
||||
|
||||
Reference in New Issue
Block a user