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>
242 lines
8.6 KiB
Plaintext
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();
|
|
}
|
|
}
|
|
}
|