Files
mabibli/MaBibli.Client/Pages/Souhaits.razor
T
mathieuandClaude Opus 5 8b8fe3be6e Ajouter la liste d'envies personnelle, son export et la bibliographie par auteur
La liste d'envies vit dans une table séparée (LivreSouhaite) plutôt que dans un
statut de plus sur Livre : un livre souhaité n'est pas possédé, et le loger dans
Livres l'aurait fait entrer dans le catalogue, les compteurs et les prêts, au
prix d'un « et qui n'est pas souhaité » à répéter dans chaque lecture. La portée
est personnelle, comme le statut de lecture — mais ici Utilisateur est une vraie
frontière : toute lecture filtre dessus.

Bibliographie : SRU BnF interrogé par bib.author, vérifié le 2026-08-18. Deux
filtres mesurés sur des réponses réelles sont indispensables — le type de
document (l'index mêle livres audio, jeux et spectacles) et surtout l'auteur
réel de la notice, « all » rapprochant les mots sur l'ensemble des auteurs :
« Émile Zola » remonte sinon toute l'œuvre de sa fille Denise Le Blond-Zola.
Les rééditions sont regroupées par clé d'œuvre (183 notices Werber -> 51
œuvres). Le rapprochement avec l'étagère se fait par titre, pas par ISBN, qui
désigne une édition et non une œuvre ; ses limites sont dites à l'écran.

Export en deux formats, tous deux du texte sans dépendance : .txt groupé par
auteur pour la librairie, .csv à séparateur point-virgule et BOM pour le
tableur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:59:06 +02:00

242 lines
8.6 KiB
Plaintext

@page "/souhaits"
@inject ServiceLivresApi Api
<PageTitle>MaBibli — liste d'envies</PageTitle>
<h1 class="titre-page">Liste d'envies</h1>
@* Le rappel n'est pas décoratif : la bibliothèque est commune, cette liste ne l'est pas.
Sans ce mot, on pourrait croire que le foyer entier voit ce qu'on note ici. *@
<p class="message-discret">
Les livres que vous aimeriez acquérir. <strong>Cette liste est la vôtre</strong> :
les autres membres du foyer ont la leur, et ne voient pas celle-ci.
Ces livres ne sont pas dans le catalogue et ne comptent pas dans la bibliothèque.
</p>
@if (_erreur is not null)
{
<p class="message-erreur" role="alert">@_erreur</p>
}
@if (_souhaits is null)
{
<p class="message-discret">Chargement…</p>
}
else
{
<section class="bloc-souhait-ajout">
<h2 class="titre-section">Ajouter une envie</h2>
@* Deux façons d'alimenter la liste : l'ISBN quand on a le livre en main (en librairie,
chez quelqu'un), la saisie libre quand on n'a qu'un titre. La bibliographie d'un
auteur, elle, alimente la liste depuis l'écran des auteurs. *@
<div class="champ">
<label class="champ-libelle" for="isbn-souhait">Pré-remplir depuis un ISBN</label>
<div class="ligne-isbn">
<input id="isbn-souhait" class="champ-saisie" type="text" inputmode="numeric"
placeholder="978…" @bind="_isbn" @bind:event="oninput" />
<button type="button" class="bouton bouton-discret"
disabled="@(_recherche || string.IsNullOrWhiteSpace(_isbn))"
@onclick="ChercherIsbnAsync">
@(_recherche ? "Recherche…" : "Chercher")
</button>
</div>
<p class="champ-aide">Facultatif : les champs ci-dessous restent modifiables.</p>
</div>
<div class="champ">
<label class="champ-libelle" for="titre-souhait">Titre <span class="obligatoire">*</span></label>
<input id="titre-souhait" class="champ-saisie" type="text" @bind="_saisie.Titre" @bind:event="oninput" />
</div>
<div class="champ">
<label class="champ-libelle" for="auteur-souhait">Auteur</label>
<input id="auteur-souhait" class="champ-saisie" type="text" @bind="_saisie.Auteur" @bind:event="oninput" />
</div>
<div class="champ">
<label class="champ-libelle" for="note-souhait">Note</label>
<input id="note-souhait" class="champ-saisie" type="text" @bind="_saisie.Note" @bind:event="oninput"
placeholder="« demandé à Noël », « en poche seulement »…" />
</div>
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal"
disabled="@(_ajout || string.IsNullOrWhiteSpace(_saisie.Titre))"
@onclick="AjouterAsync">
Ajouter à ma liste
</button>
</div>
</section>
@if (_souhaits.Count == 0)
{
<p class="message-discret">
Votre liste est vide. Ajoutez un livre ci-dessus, ou ouvrez la bibliographie d'un
auteur depuis <a href="auteurs">la liste des auteurs</a>.
</p>
}
else
{
<p class="compteur">@_souhaits.Count livre@(_souhaits.Count > 1 ? "s" : "") souhaité@(_souhaits.Count > 1 ? "s" : "")</p>
@* Un seul bloc d'export, en haut de liste : c'est l'action qu'on vient chercher quand on
part en librairie, elle ne doit pas être au bout d'un défilement. *@
<div class="actions-export">
<a class="bouton bouton-discret" href="@ServiceLivresApi.UrlExportSouhaits(FormatExportSouhaits.Texte)" download>
Exporter en texte
</a>
<a class="bouton bouton-discret" href="@ServiceLivresApi.UrlExportSouhaits(FormatExportSouhaits.Csv)" download>
Exporter en CSV
</a>
</div>
<ul class="liste-souhaits">
@foreach (var souhait in _souhaits)
{
<li class="carte-souhait" @key="souhait.Id">
<Couverture Url="@souhait.CoverUrl" Titre="@souhait.Titre"
Classe="couverture-petite" Agrandissable="true" />
<div class="carte-corps">
<p class="carte-titre">@souhait.Titre</p>
@if (!string.IsNullOrWhiteSpace(souhait.Auteur))
{
<p class="carte-auteur">@souhait.Auteur</p>
}
<p class="carte-details">
@if (!string.IsNullOrWhiteSpace(souhait.Editeur))
{
<span>@souhait.Editeur</span>
}
@if (!string.IsNullOrWhiteSpace(souhait.Annee))
{
<span>@souhait.Annee</span>
}
@if (!string.IsNullOrWhiteSpace(souhait.Isbn))
{
<span class="etiquette">ISBN @souhait.Isbn</span>
}
</p>
@if (!string.IsNullOrWhiteSpace(souhait.Note))
{
<p class="souhait-note">@souhait.Note</p>
}
</div>
<button type="button" class="bouton bouton-compact bouton-danger"
disabled="@_enCours" @onclick="() => SupprimerAsync(souhait)"
aria-label="@($"Retirer « {souhait.Titre} » de la liste d'envies")">
Retirer
</button>
</li>
}
</ul>
}
}
<nav class="actions-flottantes">
<a class="bouton bouton-discret" href="">Retour au catalogue</a>
<a class="bouton bouton-discret" href="auteurs">Auteurs</a>
</nav>
@code {
private IReadOnlyList<SouhaitDto>? _souhaits;
private EnregistrementSouhait _saisie = new();
private string _isbn = string.Empty;
private bool _ajout;
private bool _recherche;
private bool _enCours;
private string? _erreur;
protected override Task OnInitializedAsync() => ChargerAsync();
private async Task ChargerAsync()
{
try
{
_souhaits = await Api.ListerSouhaitsAsync();
}
catch (Exception ex)
{
_erreur = $"Impossible de charger la liste d'envies : {ex.Message}";
}
}
/// <summary>
/// Pré-remplit le formulaire depuis la cascade ISBN existante (BnF → OpenLibrary).
/// </summary>
/// <remarks>
/// On retient le premier candidat sans demander de choisir : contrairement à l'ajout au
/// catalogue, une envie ne fige pas d'édition — l'utilisateur achètera celle qu'il trouvera.
/// Tous les champs restent modifiables.
/// </remarks>
private async Task ChercherIsbnAsync()
{
_recherche = true;
_erreur = null;
try
{
var resultat = await Api.ChercherIsbnAsync(_isbn.Trim());
var candidat = resultat?.Candidats.FirstOrDefault();
if (candidat is null)
{
_erreur = $"Aucune notice trouvée pour « {_isbn.Trim()} ». Complétez à la main.";
return;
}
_saisie.Titre = candidat.Titre;
_saisie.Auteur = string.Join(", ", candidat.Auteurs);
_saisie.Editeur = candidat.Editeur;
_saisie.Annee = candidat.Annee;
_saisie.Isbn = _isbn.Trim();
_saisie.CoverUrl = candidat.CoverUrl;
}
catch (Exception ex)
{
_erreur = $"La recherche a échoué : {ex.Message}";
}
finally
{
_recherche = false;
}
}
private async Task AjouterAsync()
{
_ajout = true;
_erreur = null;
var resultat = await Api.AjouterSouhaitAsync(_saisie);
_ajout = false;
if (!resultat.EstOk)
{
_erreur = resultat.Erreur;
return;
}
_saisie = new EnregistrementSouhait();
_isbn = string.Empty;
await ChargerAsync();
}
private async Task SupprimerAsync(SouhaitDto souhait)
{
_enCours = true;
_erreur = await Api.SupprimerSouhaitAsync(souhait.Id);
_enCours = false;
if (_erreur is null)
{
await ChargerAsync();
}
}
}