Files
chesscubing/ChessCubing.App/Pages/UserPage.razor

965 lines
39 KiB
Plaintext

@page "/utilisateur"
@page "/utilisateur.html"
@using System.ComponentModel.DataAnnotations
@using System.Net
@using System.Net.Http.Json
@using ChessCubing.App.Models.Users
@implements IDisposable
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject HttpClient Http
@inject SocialRealtimeService Realtime
<PageTitle>ChessCubing Arena | Utilisateur</PageTitle>
<PageBody BodyClass="home-body" />
<div class="ambient ambient-left"></div>
<div class="ambient ambient-right"></div>
<div class="rules-shell">
<header class="hero hero-home user-hero">
<div class="hero-copy">
<p class="eyebrow">Espace utilisateur</p>
<h1>Profil joueur, amis et invitations</h1>
<p class="lead">
Cette page regroupe le profil du site, la gestion des amis ChessCubing,
ainsi que les invitations recues ou envoyees. Les confirmations de partie
sont ensuite synchronisees en direct avec SignalR.
</p>
<div class="hero-actions">
<a class="button secondary" href="application.html">Ouvrir l'application</a>
<a class="button ghost" href="reglement.html">Voir les formats</a>
</div>
</div>
<aside class="hero-preview">
<div class="preview-card">
<p class="micro-label">Temps reel</p>
<strong>Presence et invitations synchronisees</strong>
<p>
Les amis connectes apparaissent en direct, et les invitations de
partie peuvent etre confirmees depuis un autre device.
</p>
</div>
<div class="preview-banner">
<span class="mini-chip">Etat du compte</span>
<strong>@HeroStatusTitle</strong>
<p>@HeroStatusDescription</p>
</div>
</aside>
</header>
<main class="rules-grid">
@if (!IsAuthenticated)
{
<section class="panel panel-wide cta-panel">
<div class="section-heading">
<div>
<p class="eyebrow">Connexion requise</p>
<h2>Connecte-toi pour gerer ton profil et tes amis</h2>
</div>
<p class="section-copy">
Utilise les boutons Se connecter ou Creer un compte dans le menu
en haut de page, puis reviens ici pour enregistrer tes informations
et inviter d'autres joueurs.
</p>
</div>
<div class="source-grid">
<a class="source-card" href="index.html">
<span class="micro-label">Accueil</span>
<strong>Retourner au site</strong>
<p>Revenir a la presentation generale de ChessCubing Arena.</p>
</a>
<a class="source-card" href="application.html">
<span class="micro-label">Application</span>
<strong>Ouvrir l'arbitrage</strong>
<p>Configurer un match pendant que le compte reste disponible dans le menu.</p>
</a>
<a class="source-card" href="reglement.html">
<span class="micro-label">Reglement</span>
<strong>Relire les formats</strong>
<p>Retrouver rapidement les differences entre Twice et Time.</p>
</a>
</div>
</section>
}
else if (IsLoading)
{
<section class="panel panel-wide">
<p class="eyebrow">Chargement</p>
<h2>Recuperation de l'espace utilisateur</h2>
<p class="section-copy">Le serveur recharge le profil, les amis et les invitations de ce compte.</p>
</section>
}
else if (!string.IsNullOrWhiteSpace(LoadError))
{
<section class="panel panel-wide">
<p class="eyebrow">Serveur</p>
<h2>Impossible de charger l'espace utilisateur</h2>
<p class="profile-feedback error">@LoadError</p>
<div class="hero-actions">
<button class="button secondary" type="button" @onclick="LoadProfileAsync">Reessayer</button>
</div>
</section>
}
else if (Profile is not null)
{
<section class="panel panel-wide user-profile-panel">
<div class="section-heading user-profile-heading">
<div>
<p class="eyebrow">Profil joueur</p>
<h2>@Profile.DisplayName</h2>
</div>
<div class="user-profile-chips">
<span class="mini-chip admin-chip-outline">@Profile.Username</span>
@if (!string.IsNullOrWhiteSpace(Profile.Email))
{
<span class="mini-chip admin-chip-neutral">@Profile.Email</span>
}
</div>
</div>
<p class="user-profile-note">Compte Keycloak relie a une fiche site compacte stockee en MySQL.</p>
<div class="user-profile-summary-grid">
<article class="user-summary-card">
<span class="micro-label">Cree</span>
<strong>@FormatDate(Profile.CreatedUtc)</strong>
</article>
<article class="user-summary-card">
<span class="micro-label">Mis a jour</span>
<strong>@FormatDate(Profile.UpdatedUtc)</strong>
</article>
<article class="user-summary-card">
<span class="micro-label">Club</span>
<strong>@(Profile.Club ?? "A definir")</strong>
</article>
<article class="user-summary-card">
<span class="micro-label">Ville</span>
<strong>@(Profile.City ?? "A definir")</strong>
</article>
<article class="user-summary-card">
<span class="micro-label">Format</span>
<strong>@(Profile.PreferredFormat ?? "A definir")</strong>
</article>
<article class="user-summary-card">
<span class="micro-label">Cube favori</span>
<strong>@(Profile.FavoriteCube ?? "A definir")</strong>
</article>
</div>
<div class="callout user-profile-bio">
<span class="micro-label">Bio</span>
<p>@(Profile.Bio ?? "Aucune bio enregistree pour le moment.")</p>
</div>
@if (!string.IsNullOrWhiteSpace(SaveError))
{
<p class="profile-feedback error">@SaveError</p>
}
@if (!string.IsNullOrWhiteSpace(SaveMessage))
{
<p class="profile-feedback success">@SaveMessage</p>
}
<EditForm Model="@Form" OnValidSubmit="SaveProfileAsync">
<DataAnnotationsValidator />
<div class="profile-form-grid user-profile-form-grid">
<label class="field user-span-2">
<span>Nom affiche</span>
<InputText @bind-Value="Form.DisplayName" />
<ValidationMessage For="@(() => Form.DisplayName)" />
</label>
<label class="field">
<span>Club</span>
<InputText @bind-Value="Form.Club" />
<ValidationMessage For="@(() => Form.Club)" />
</label>
<label class="field">
<span>Ville</span>
<InputText @bind-Value="Form.City" />
<ValidationMessage For="@(() => Form.City)" />
</label>
<label class="field">
<span>Format prefere</span>
<InputSelect @bind-Value="Form.PreferredFormat">
<option value="">Aucune preference</option>
<option value="Twice">Twice</option>
<option value="Time">Time</option>
<option value="Les deux">Les deux</option>
</InputSelect>
<ValidationMessage For="@(() => Form.PreferredFormat)" />
</label>
<label class="field user-span-2">
<span>Cube favori</span>
<InputText @bind-Value="Form.FavoriteCube" />
<ValidationMessage For="@(() => Form.FavoriteCube)" />
</label>
<label class="field user-span-full">
<span>Bio</span>
<InputTextArea @bind-Value="Form.Bio" rows="2" />
<ValidationMessage For="@(() => Form.Bio)" />
</label>
</div>
<div class="profile-actions user-profile-actions">
<button class="button secondary small" type="submit" disabled="@IsSaving">
@(IsSaving ? "Enregistrement..." : "Enregistrer")
</button>
</div>
</EditForm>
</section>
<section class="panel panel-wide user-social-panel">
<div class="section-heading user-profile-heading">
<div>
<p class="eyebrow">Reseau joueur</p>
<h2>Amis et invitations</h2>
</div>
<div class="hero-actions">
<button class="button ghost small icon-button" type="button" title="Rafraichir" aria-label="Rafraichir" @onclick="ReloadSocialAsync">
<span class="material-icons action-icon" aria-hidden="true">refresh</span>
</button>
</div>
</div>
<p class="user-profile-note">Invite des joueurs du site, suis les invitations en cours et retrouve rapidement les amis connectes.</p>
@if (!string.IsNullOrWhiteSpace(SocialLoadError))
{
<p class="profile-feedback error">@SocialLoadError</p>
}
@if (!string.IsNullOrWhiteSpace(SocialActionError))
{
<p class="profile-feedback error">@SocialActionError</p>
}
@if (!string.IsNullOrWhiteSpace(SocialActionMessage))
{
<p class="profile-feedback success">@SocialActionMessage</p>
}
<form class="social-search-form" @onsubmit="HandleSearchSubmit" @onsubmit:preventDefault="true">
<label class="field">
<span>Rechercher un joueur</span>
<input @bind="SearchQuery" @bind:event="oninput" type="text" maxlength="80" placeholder="Nom, pseudo, club, ville..." />
</label>
<div class="social-search-actions">
<button class="button secondary small" type="submit" disabled="@IsSearching">
@(IsSearching ? "Recherche..." : "Chercher")
</button>
</div>
</form>
@if (!string.IsNullOrWhiteSpace(SearchError))
{
<p class="profile-feedback error">@SearchError</p>
}
@if (SearchResults.Length > 0)
{
<div class="social-search-results">
@foreach (var result in SearchResults)
{
<article class="social-item">
<div class="social-item-meta">
<div class="social-item-title-row">
<strong>@result.DisplayName</strong>
<span class="@BuildPresenceClass(result.Subject, result.IsOnline)">@BuildPresenceLabel(result.Subject, result.IsOnline)</span>
</div>
<span class="social-item-subtitle">@result.Username</span>
@if (!string.IsNullOrWhiteSpace(result.Club) || !string.IsNullOrWhiteSpace(result.City))
{
<span class="social-item-caption">@JoinNonEmpty(result.Club, result.City)</span>
}
</div>
<div class="social-item-actions">
@if (result.IsFriend)
{
<span class="mini-chip admin-chip-neutral">Deja ami</span>
}
else if (FindReceivedInvitation(result.Subject) is { } receivedInvitation)
{
<button class="button secondary small" type="button" @onclick="() => AcceptInvitationAsync(receivedInvitation.InvitationId)">
Accepter
</button>
<button class="button ghost small" type="button" @onclick="() => DeclineInvitationAsync(receivedInvitation.InvitationId)">
Refuser
</button>
}
else if (FindSentInvitation(result.Subject) is { } sentInvitation)
{
<span class="mini-chip admin-chip-outline">Invitation envoyee</span>
<button class="button ghost small" type="button" @onclick="() => CancelInvitationAsync(sentInvitation.InvitationId)">
Annuler
</button>
}
else
{
<button class="button secondary small" type="button" @onclick="() => SendInvitationAsync(result.Subject)">
Inviter
</button>
}
</div>
</article>
}
</div>
}
<div class="social-columns">
<section class="social-card">
<div class="social-card-head">
<strong>Amis</strong>
<span class="mini-chip admin-chip-outline">@FriendCountLabel</span>
</div>
@if (IsSocialLoading)
{
<p class="social-empty">Chargement des amis...</p>
}
else if (SocialOverview?.Friends.Length > 0)
{
<div class="social-list">
@foreach (var friend in SocialOverview.Friends.OrderByDescending(friend => ResolveOnlineStatus(friend.Subject, friend.IsOnline)).ThenBy(friend => friend.DisplayName, StringComparer.OrdinalIgnoreCase))
{
<article class="social-item">
<div class="social-item-meta">
<div class="social-item-title-row">
<strong>@friend.DisplayName</strong>
<span class="@BuildPresenceClass(friend.Subject, friend.IsOnline)">@BuildPresenceLabel(friend.Subject, friend.IsOnline)</span>
</div>
<span class="social-item-subtitle">@friend.Username</span>
@if (!string.IsNullOrWhiteSpace(friend.Club) || !string.IsNullOrWhiteSpace(friend.City))
{
<span class="social-item-caption">@JoinNonEmpty(friend.Club, friend.City)</span>
}
</div>
<div class="social-item-actions">
<button class="button ghost small" type="button" @onclick="() => RemoveFriendAsync(friend.Subject)">
Retirer
</button>
</div>
</article>
}
</div>
}
else
{
<p class="social-empty">Aucun ami pour le moment. Utilise la recherche ci-dessus pour commencer.</p>
}
</section>
<section class="social-card">
<div class="social-card-head">
<strong>Invitations recues</strong>
<span class="mini-chip admin-chip-outline">@ReceivedCountLabel</span>
</div>
@if (IsSocialLoading)
{
<p class="social-empty">Chargement des invitations...</p>
}
else if (SocialOverview?.ReceivedInvitations.Length > 0)
{
<div class="social-list">
@foreach (var invitation in SocialOverview.ReceivedInvitations.OrderByDescending(invitation => invitation.CreatedUtc))
{
<article class="social-item">
<div class="social-item-meta">
<div class="social-item-title-row">
<strong>@invitation.DisplayName</strong>
<span class="@BuildPresenceClass(invitation.Subject, invitation.IsOnline)">@BuildPresenceLabel(invitation.Subject, invitation.IsOnline)</span>
</div>
<span class="social-item-subtitle">@invitation.Username</span>
<span class="social-item-caption">Recue le @FormatDate(invitation.CreatedUtc)</span>
</div>
<div class="social-item-actions">
<button class="button secondary small" type="button" @onclick="() => AcceptInvitationAsync(invitation.InvitationId)">
Accepter
</button>
<button class="button ghost small" type="button" @onclick="() => DeclineInvitationAsync(invitation.InvitationId)">
Refuser
</button>
</div>
</article>
}
</div>
}
else
{
<p class="social-empty">Aucune invitation recue pour l'instant.</p>
}
</section>
<section class="social-card">
<div class="social-card-head">
<strong>Invitations envoyees</strong>
<span class="mini-chip admin-chip-outline">@SentCountLabel</span>
</div>
@if (IsSocialLoading)
{
<p class="social-empty">Chargement des invitations...</p>
}
else if (SocialOverview?.SentInvitations.Length > 0)
{
<div class="social-list">
@foreach (var invitation in SocialOverview.SentInvitations.OrderByDescending(invitation => invitation.CreatedUtc))
{
<article class="social-item">
<div class="social-item-meta">
<div class="social-item-title-row">
<strong>@invitation.DisplayName</strong>
<span class="@BuildPresenceClass(invitation.Subject, invitation.IsOnline)">@BuildPresenceLabel(invitation.Subject, invitation.IsOnline)</span>
</div>
<span class="social-item-subtitle">@invitation.Username</span>
<span class="social-item-caption">Envoyee le @FormatDate(invitation.CreatedUtc)</span>
</div>
<div class="social-item-actions">
<button class="button ghost small" type="button" @onclick="() => CancelInvitationAsync(invitation.InvitationId)">
Annuler
</button>
</div>
</article>
}
</div>
}
else
{
<p class="social-empty">Aucune invitation envoyee pour l'instant.</p>
}
</section>
</div>
</section>
}
</main>
</div>
@code {
private readonly UserProfileFormModel Form = new();
private UserProfileResponse? Profile;
private SocialOverviewResponse? SocialOverview;
private SocialSearchUserResponse[] SearchResults = [];
private bool IsAuthenticated;
private bool IsLoading = true;
private bool IsSaving;
private bool IsSocialLoading;
private bool IsSearching;
private int _knownSocialVersion;
private string? LoadError;
private string? SaveError;
private string? SaveMessage;
private string? SocialLoadError;
private string? SocialActionError;
private string? SocialActionMessage;
private string? SearchError;
private string SearchQuery = string.Empty;
private string HeroStatusTitle
=> !IsAuthenticated
? "Connexion requise"
: IsLoading
? "Chargement du profil"
: Profile?.DisplayName ?? "Profil utilisateur";
private string HeroStatusDescription
=> !IsAuthenticated
? "Le profil du site et le reseau d'amis apparaissent des qu'un compte joueur est connecte."
: IsLoading
? "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 FriendCountLabel => $"{SocialOverview?.Friends.Length ?? 0} ami(s)";
private string ReceivedCountLabel => $"{SocialOverview?.ReceivedInvitations.Length ?? 0} recue(s)";
private string SentCountLabel => $"{SocialOverview?.SentInvitations.Length ?? 0} envoyee(s)";
protected override async Task OnInitializedAsync()
{
AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
Realtime.Changed += HandleRealtimeChanged;
await Realtime.EnsureStartedAsync();
await LoadProfileAsync();
}
private void HandleAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
=> _ = InvokeAsync(LoadProfileAsync);
private void HandleRealtimeChanged()
=> _ = InvokeAsync(HandleRealtimeChangedAsync);
private async Task HandleRealtimeChangedAsync()
{
if (!IsAuthenticated)
{
StateHasChanged();
return;
}
if (_knownSocialVersion != Realtime.SocialVersion)
{
_knownSocialVersion = Realtime.SocialVersion;
await LoadSocialOverviewAsync();
if (!string.IsNullOrWhiteSpace(SearchQuery))
{
await SearchUsersAsync();
}
return;
}
StateHasChanged();
}
private async Task LoadProfileAsync()
{
LoadError = null;
SaveError = null;
SaveMessage = null;
IsLoading = true;
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated != true)
{
ResetProfileState();
ResetSocialState();
return;
}
IsAuthenticated = true;
var response = await Http.GetAsync("api/users/me");
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
ResetProfileState();
ResetSocialState();
return;
}
if (!response.IsSuccessStatusCode)
{
LoadError = await ReadErrorAsync(response, "Le profil utilisateur n'a pas pu etre charge.");
Profile = null;
ResetSocialState();
return;
}
Profile = await response.Content.ReadFromJsonAsync<UserProfileResponse>();
if (Profile is null)
{
LoadError = "Le serveur a retourne une reponse vide.";
ResetSocialState();
return;
}
FillForm(Profile);
await LoadSocialOverviewAsync();
}
catch (HttpRequestException)
{
LoadError = "Le service utilisateur est temporairement indisponible.";
Profile = null;
ResetSocialState();
}
catch (TaskCanceledException)
{
LoadError = "La reponse du service utilisateur a pris trop de temps.";
Profile = null;
ResetSocialState();
}
finally
{
IsLoading = false;
StateHasChanged();
}
}
private async Task LoadSocialOverviewAsync()
{
SocialLoadError = null;
IsSocialLoading = true;
try
{
var response = await Http.GetAsync("api/social/overview");
if (!response.IsSuccessStatusCode)
{
SocialLoadError = await ReadErrorAsync(response, "Le reseau social n'a pas pu etre charge.");
SocialOverview = null;
SearchResults = [];
return;
}
SocialOverview = await response.Content.ReadFromJsonAsync<SocialOverviewResponse>();
SocialOverview ??= new SocialOverviewResponse();
_knownSocialVersion = Realtime.SocialVersion;
}
catch (HttpRequestException)
{
SocialLoadError = "Le service social est temporairement indisponible.";
SocialOverview = null;
}
catch (TaskCanceledException)
{
SocialLoadError = "Le chargement du reseau social a pris trop de temps.";
SocialOverview = null;
}
finally
{
IsSocialLoading = false;
StateHasChanged();
}
}
private async Task SaveProfileAsync()
{
if (IsSaving || !IsAuthenticated)
{
return;
}
IsSaving = true;
SaveError = null;
SaveMessage = null;
try
{
var payload = new UpdateUserProfileRequest
{
DisplayName = Form.DisplayName,
Club = Form.Club,
City = Form.City,
PreferredFormat = Form.PreferredFormat,
FavoriteCube = Form.FavoriteCube,
Bio = Form.Bio,
};
var response = await Http.PutAsJsonAsync("api/users/me", payload);
if (!response.IsSuccessStatusCode)
{
SaveError = await ReadErrorAsync(response, "L'enregistrement du profil a echoue.");
return;
}
Profile = await response.Content.ReadFromJsonAsync<UserProfileResponse>();
if (Profile is null)
{
SaveError = "Le serveur a retourne une reponse vide apres la sauvegarde.";
return;
}
FillForm(Profile);
SaveMessage = "Le profil utilisateur a bien ete enregistre.";
}
catch (HttpRequestException)
{
SaveError = "Le service utilisateur est temporairement indisponible.";
}
catch (TaskCanceledException)
{
SaveError = "La sauvegarde a pris trop de temps.";
}
finally
{
IsSaving = false;
}
}
private Task HandleSearchSubmit()
=> SearchUsersAsync();
private async Task SearchUsersAsync()
{
SearchError = null;
SocialActionError = null;
SocialActionMessage = null;
var normalizedQuery = SearchQuery.Trim();
if (string.IsNullOrWhiteSpace(normalizedQuery))
{
SearchResults = [];
StateHasChanged();
return;
}
IsSearching = true;
try
{
var response = await Http.GetAsync($"api/social/search?query={Uri.EscapeDataString(normalizedQuery)}");
if (!response.IsSuccessStatusCode)
{
SearchError = await ReadErrorAsync(response, "La recherche de joueurs a echoue.");
SearchResults = [];
return;
}
SearchResults = await response.Content.ReadFromJsonAsync<SocialSearchUserResponse[]>() ?? [];
}
catch (HttpRequestException)
{
SearchError = "Le service social est temporairement indisponible.";
SearchResults = [];
}
catch (TaskCanceledException)
{
SearchError = "La recherche a pris trop de temps.";
SearchResults = [];
}
finally
{
IsSearching = false;
StateHasChanged();
}
}
private async Task ReloadSocialAsync()
{
await LoadSocialOverviewAsync();
if (!string.IsNullOrWhiteSpace(SearchQuery))
{
await SearchUsersAsync();
}
}
private async Task SendInvitationAsync(string subject)
{
await RunSocialActionAsync(
async () =>
{
var payload = new SendFriendInvitationRequest { TargetSubject = subject };
var response = await Http.PostAsJsonAsync("api/social/invitations", payload);
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(await ReadErrorAsync(response, "L'invitation n'a pas pu etre envoyee."));
}
},
"Invitation d'ami envoyee.");
}
private async Task AcceptInvitationAsync(long invitationId)
{
await RunSocialActionAsync(
async () =>
{
var response = await Http.PostAsync($"api/social/invitations/{invitationId}/accept", content: null);
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(await ReadErrorAsync(response, "L'invitation n'a pas pu etre acceptee."));
}
},
"Invitation acceptee. Le joueur a ete ajoute aux amis.");
}
private async Task DeclineInvitationAsync(long invitationId)
{
await RunSocialActionAsync(
async () =>
{
var response = await Http.PostAsync($"api/social/invitations/{invitationId}/decline", content: null);
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(await ReadErrorAsync(response, "L'invitation n'a pas pu etre refusee."));
}
},
"Invitation refusee.");
}
private async Task CancelInvitationAsync(long invitationId)
{
await RunSocialActionAsync(
async () =>
{
var response = await Http.DeleteAsync($"api/social/invitations/{invitationId}");
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(await ReadErrorAsync(response, "L'invitation n'a pas pu etre annulee."));
}
},
"Invitation annulee.");
}
private async Task RemoveFriendAsync(string friendSubject)
{
await RunSocialActionAsync(
async () =>
{
var response = await Http.DeleteAsync($"api/social/friends/{Uri.EscapeDataString(friendSubject)}");
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(await ReadErrorAsync(response, "L'ami n'a pas pu etre retire."));
}
},
"Relation d'amitie retiree.");
}
private async Task RunSocialActionAsync(Func<Task> action, string successMessage)
{
SocialActionError = null;
SocialActionMessage = null;
try
{
await action();
SocialActionMessage = successMessage;
await LoadSocialOverviewAsync();
if (!string.IsNullOrWhiteSpace(SearchQuery))
{
await SearchUsersAsync();
}
}
catch (InvalidOperationException exception)
{
SocialActionError = exception.Message;
}
catch (HttpRequestException)
{
SocialActionError = "Le service social est temporairement indisponible.";
}
catch (TaskCanceledException)
{
SocialActionError = "L'action sociale a pris trop de temps.";
}
}
private void ResetProfileState()
{
IsAuthenticated = false;
Profile = null;
Form.Reset();
}
private void ResetSocialState()
{
SocialOverview = null;
SearchResults = [];
SearchQuery = string.Empty;
SearchError = null;
SocialLoadError = null;
SocialActionError = null;
SocialActionMessage = null;
_knownSocialVersion = Realtime.SocialVersion;
}
private void FillForm(UserProfileResponse profile)
{
Form.DisplayName = profile.DisplayName;
Form.Club = profile.Club;
Form.City = profile.City;
Form.PreferredFormat = profile.PreferredFormat;
Form.FavoriteCube = profile.FavoriteCube;
Form.Bio = profile.Bio;
}
private bool ResolveOnlineStatus(string subject, bool fallbackStatus)
=> Realtime.GetKnownOnlineStatus(subject) ?? fallbackStatus;
private string BuildPresenceClass(string subject, bool fallbackStatus)
=> ResolveOnlineStatus(subject, fallbackStatus)
? "presence-badge online"
: "presence-badge";
private string BuildPresenceLabel(string subject, bool fallbackStatus)
=> ResolveOnlineStatus(subject, fallbackStatus)
? "En ligne"
: "Hors ligne";
private SocialInvitationResponse? FindReceivedInvitation(string subject)
=> SocialOverview?.ReceivedInvitations.FirstOrDefault(invitation => string.Equals(invitation.Subject, subject, StringComparison.Ordinal));
private SocialInvitationResponse? FindSentInvitation(string subject)
=> SocialOverview?.SentInvitations.FirstOrDefault(invitation => string.Equals(invitation.Subject, subject, StringComparison.Ordinal));
private static string JoinNonEmpty(params string?[] values)
=> string.Join(" • ", values.Where(value => !string.IsNullOrWhiteSpace(value)));
private static async Task<string> ReadErrorAsync(HttpResponseMessage response, string fallbackMessage)
{
try
{
var error = await response.Content.ReadFromJsonAsync<ApiErrorMessage>();
if (!string.IsNullOrWhiteSpace(error?.Message))
{
return error.Message;
}
}
catch
{
}
return response.StatusCode switch
{
HttpStatusCode.Unauthorized => "La session a expire. Reconnecte-toi puis recharge la page.",
_ => fallbackMessage,
};
}
private static string FormatDate(DateTime value)
=> value.ToLocalTime().ToString("dd MMM yyyy 'a' HH:mm", CultureInfo.GetCultureInfo("fr-FR"));
public void Dispose()
{
AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
Realtime.Changed -= HandleRealtimeChanged;
}
private sealed class ApiErrorMessage
{
public string? Message { get; set; }
}
private sealed class UserProfileFormModel
{
[MaxLength(120, ErrorMessage = "Le nom affiche doit rester sous 120 caracteres.")]
public string? DisplayName { get; set; }
[MaxLength(120, ErrorMessage = "Le club doit rester sous 120 caracteres.")]
public string? Club { get; set; }
[MaxLength(120, ErrorMessage = "La ville doit rester sous 120 caracteres.")]
public string? City { get; set; }
[MaxLength(40, ErrorMessage = "Le format prefere doit rester sous 40 caracteres.")]
public string? PreferredFormat { get; set; }
[MaxLength(120, ErrorMessage = "Le cube favori doit rester sous 120 caracteres.")]
public string? FavoriteCube { get; set; }
[MaxLength(1200, ErrorMessage = "La bio doit rester sous 1200 caracteres.")]
public string? Bio { get; set; }
public void Reset()
{
DisplayName = string.Empty;
Club = string.Empty;
City = string.Empty;
PreferredFormat = string.Empty;
FavoriteCube = string.Empty;
Bio = string.Empty;
}
}
}