Files
mabibli/MaBibli.Client/Pages/EditionSouhait.razor
T

210 lines
7.8 KiB
Plaintext

@page "/souhaits/{Id:int}/edition"
@inject ServiceLivresApi Api
@inject EtatReseau Reseau
@inject NavigationManager Navigation
@implements IDisposable
@*
Écran dédié plutôt qu'un formulaire déplié dans la liste : convention du projet depuis la
fiche livre — le mode est dans l'ADRESSE, jamais dans un booléen interne. C'est ce qui rend
le bouton « retour » du navigateur une sortie d'édition naturelle, et ce qui empêche de
modifier une envie en faisant défiler sa liste.
⚠️ Cette page ne partage aucun paramètre avec une autre route : contrairement à la fiche
livre ou à la fiche série, elle n'a pas besoin de s'abonner à LocationChanged. L'abonnement
ci-dessous ne sert qu'à suivre l'état du réseau.
*@
<PageTitle>MaBibli — modifier une envie</PageTitle>
<h1 class="titre-page">Modifier une envie</h1>
@if (_erreur is not null)
{
<p class="message-erreur" role="alert">@_erreur</p>
}
@if (_introuvable)
{
@* Hors-ligne, « supprimée » et « pas encore enregistrée ici » sont indiscernables : on ne
tranche pas à la place de l'utilisateur. Même règle que pour une fiche livre absente. *@
<p class="message-discret">
@(Reseau.EnLigne
? "Cette envie n'existe plus dans votre liste."
: "Cette envie n'est pas dans les données enregistrées sur cet appareil.")
</p>
}
else if (_saisie is null)
{
<Patience Message="Ouverture de l'envie…" />
}
else
{
<div class="formulaire">
@* Même ligne que dans le formulaire d'un ouvrage (FormulaireLivre) : un aperçu quand il y
a quelque chose à montrer, puis un champ d'adresse ordinaire, au même titre que les
autres. *@
@if (!string.IsNullOrEmpty(_saisie.CoverUrl) || !string.IsNullOrWhiteSpace(_saisie.Titre))
{
<div class="formulaire-apercu">
<Couverture Url="@_saisie.CoverUrl" Titre="@_saisie.Titre" Classe="couverture-moyenne" />
</div>
}
<label class="champ">
<span class="champ-libelle">Titre <span class="obligatoire" aria-hidden="true">*</span></span>
<input class="champ-saisie" type="text" @bind="_saisie.Titre" @bind:event="oninput"
autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">Auteur</span>
<input class="champ-saisie" type="text" @bind="_saisie.Auteur" @bind:event="oninput"
autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">Éditeur</span>
<input class="champ-saisie" type="text" @bind="_saisie.Editeur" @bind:event="oninput"
autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">Année</span>
<input class="champ-saisie" type="text" @bind="_saisie.Annee" @bind:event="oninput"
placeholder="2012" autocomplete="off" />
</label>
@* Le champ de saisie garde la valeur nue : découper à la frappe se battrait avec le
curseur. Les tirets sont une affaire d'affichage, pas de saisie. *@
<label class="champ">
<span class="champ-libelle">ISBN <span class="champ-aide">(facultatif)</span></span>
<input class="champ-saisie" type="text" inputmode="numeric"
@bind="_saisie.Isbn" @bind:event="oninput" placeholder="978…" autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">URL de couverture</span>
<input class="champ-saisie" type="url" @bind="_saisie.CoverUrl" @bind:event="oninput"
placeholder="https://…" autocomplete="off" />
</label>
<label class="champ">
<span class="champ-libelle">Note</span>
<input class="champ-saisie" type="text" @bind="_saisie.Note" @bind:event="oninput"
placeholder="« demandé à Noël », « en poche seulement »…" autocomplete="off" />
</label>
@if (!Reseau.EnLigne)
{
<p class="message-avertissement" role="status">@EtatReseau.MotifHorsLigne</p>
}
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal"
disabled="@(_enCours || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_saisie.Titre))"
title="@MotifBlocage" @onclick="EnregistrerAsync">
@(_enCours ? "Enregistrement…" : "Enregistrer")
</button>
<a class="bouton bouton-discret" href="souhaits">Annuler</a>
</div>
</div>
}
@code {
[Parameter] public int Id { get; set; }
private EnregistrementSouhait? _saisie;
private bool _introuvable;
private bool _enCours;
private string? _erreur;
/// <summary>
/// La couverture telle que chargée — éventuellement déduite de l'ISBN (voir <c>Projeter</c>
/// côté serveur), pas forcément ce qu'il y a réellement en base. Sert uniquement à détecter,
/// à l'enregistrement, si l'utilisateur a touché au champ.
/// </summary>
private string? _couvertureChargee;
protected override async Task OnInitializedAsync()
{
Reseau.Change += SurChangementReseau;
await ChargerAsync();
}
private void SurChangementReseau() => _ = InvokeAsync(StateHasChanged);
public void Dispose() => Reseau.Change -= SurChangementReseau;
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
/// <summary>
/// Reprend l'envie depuis la <b>liste</b>, et non depuis un point d'entrée dédié.
/// </summary>
/// <remarks>
/// L'API n'expose pas <c>GET /api/souhaits/{id}</c>, et il n'y a pas lieu de l'ajouter : la
/// liste entière est déjà chargée, déjà mise en instantané, et c'est ce qui rend cet écran
/// consultable hors-ligne — même si l'enregistrement, lui, y est refusé.
/// </remarks>
private async Task ChargerAsync()
{
var souhaits = await Api.ListerSouhaitsAsync();
var souhait = souhaits.FirstOrDefault(s => s.Id == Id);
if (souhait is null)
{
_introuvable = true;
return;
}
_couvertureChargee = souhait.CoverUrl;
_saisie = new EnregistrementSouhait
{
Titre = souhait.Titre,
Auteur = souhait.Auteur,
Editeur = souhait.Editeur,
Annee = souhait.Annee,
Isbn = souhait.Isbn,
CoverUrl = souhait.CoverUrl,
Note = souhait.Note,
};
}
private async Task EnregistrerAsync()
{
if (_saisie is null)
{
return;
}
// ⚠️ Envoyée seulement si elle a changé depuis le chargement, et non à chaque
// enregistrement : la valeur affichée peut être une DÉDUCTION à la lecture (formule
// OpenLibrary à ISBN connu, voir `Projeter`) plutôt que ce qu'il y a réellement en base.
// La renvoyer telle quelle à chaque correction de titre la figerait en base, et une
// correction d'ISBN qui suit ne la referait plus jouer.
_saisie.CoverUrlModifiee = !string.Equals(
_saisie.CoverUrl?.Trim() ?? string.Empty,
_couvertureChargee?.Trim() ?? string.Empty,
StringComparison.Ordinal);
_enCours = true;
_erreur = null;
var resultat = await Api.ModifierSouhaitAsync(Id, _saisie);
_enCours = false;
if (!resultat.EstOk)
{
// Le message vient de l'API : doublon d'une autre envie, ISBN invalide, ou hors
// ligne. Aucun code HTTP brut n'arrive jusqu'ici.
_erreur = resultat.Erreur;
return;
}
Navigation.NavigateTo("souhaits");
}
}