@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 ChessCubing Arena | Utilisateur

Espace utilisateur

Profil joueur, amis et invitations

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.

@if (!IsAuthenticated) {

Connexion requise

Connecte-toi pour gerer ton profil et tes amis

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.

} else if (IsLoading) {

Chargement

Recuperation de l'espace utilisateur

Le serveur recharge le profil, les amis et les invitations de ce compte.

} else if (!string.IsNullOrWhiteSpace(LoadError)) {

Serveur

Impossible de charger l'espace utilisateur

@LoadError

} else if (Profile is not null) {
@if (!string.IsNullOrWhiteSpace(SocialLoadError)) {

@SocialLoadError

} @if (!string.IsNullOrWhiteSpace(SocialActionError)) {

@SocialActionError

} @if (!string.IsNullOrWhiteSpace(SocialActionMessage)) {

@SocialActionMessage

} @if (!string.IsNullOrWhiteSpace(SearchError)) {

@SearchError

} @if (SearchResults.Length > 0) { }
}
@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 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(); 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(); 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(); 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() ?? []; } 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 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 ReadErrorAsync(HttpResponseMessage response, string fallbackMessage) { try { var error = await response.Content.ReadFromJsonAsync(); 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; } } }