Ajout du moteur de recherche dans le header

This commit is contained in:
Loic Masi
2026-03-10 21:38:40 +01:00
parent ae1ee725a9
commit 47b3c0bdd7
12 changed files with 334 additions and 93 deletions

View File

@@ -10,20 +10,20 @@ namespace Webzine.Repository.Contracts
// void Delete(Titre titre); // void Delete(Titre titre);
// Titre Find(int idTitre); Titre? Find(int idTitre);
// IEnumerable<Titre> FindTitres(int offset, int limit); // IEnumerable<Titre> FindTitres(int offset, int limit);
// IEnumerable<Titre> FindAll(); IEnumerable<Titre> FindAll();
// void IncrementNbLectures(Titre titre); // void IncrementNbLectures(Titre titre);
// void IncrementNbLikes(Titre titre); // void IncrementNbLikes(Titre titre);
// IEnumerable<Titre> Search(string mot); IEnumerable<Titre> Search(string mot);
// IEnumerable<Titre> SearchByStyle(string libelle); IEnumerable<Titre> SearchByStyle(string libelle);
// void Update(Titre titre); // void Update(Titre titre);
} }
} }

View File

@@ -1,18 +1,71 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
using Webzine.Repository.Contracts;
namespace Webzine.Repository; namespace Webzine.Repository;
public class LocalEntityRepository /// <summary>
/// Classe qui permet d'initialiser un jeu de données
/// pour tester l'application
/// </summary>
public class LocalEntityRepository : ITitreRepository
{ {
private readonly ILogger<LocalEntityRepository> _logger; private readonly ILogger<LocalEntityRepository> _logger;
private readonly List<Titre> _titres;
/// <summary> /// <summary>
/// Initialise une nouvelle instance du <see cref="LocalEntityRepository"/> avec un service de journalisation injecté. /// Initialise une nouvelle instance du <see cref="LocalEntityRepository"/> avec un service de journalisation injecte.
/// </summary> /// </summary>
/// <param name="logger">Service de journalisation injecté pour suivre les opérations du repository.</param> /// <param name="logger">Service de journalisation injecte pour suivre les operations du repository.</param>
public LocalEntityRepository(ILogger<LocalEntityRepository> logger) public LocalEntityRepository(ILogger<LocalEntityRepository> logger)
{ {
this._logger = logger; _logger = logger;
this._logger.LogDebug(1, "NLog injected into LocalEntityRepository"); _logger.LogDebug(1, "NLog injected into LocalEntityRepository");
var factory = new DataFactory();
var artistes = factory.GenerateArtists(10);
var styles = factory.GenerateStyles(10);
_titres = factory.GenerateTitres(30, artistes, styles);
factory.GenerateCommentaires(50, _titres);
} }
}
public IEnumerable<Titre> Search(string mot)
{
if (string.IsNullOrWhiteSpace(mot))
{
return Enumerable.Empty<Titre>();
}
return _titres
.Where(t => !string.IsNullOrWhiteSpace(t.Libelle)
&& t.Libelle.Contains(mot, StringComparison.OrdinalIgnoreCase))
.OrderBy(t => t.Libelle)
.ToList();
}
public Titre? Find(int idTitre)
{
return _titres.FirstOrDefault(t => t.IdTitre == idTitre);
}
public IEnumerable<Titre> FindAll()
{
return _titres;
}
public IEnumerable<Titre> SearchByStyle(string libelle)
{
if (string.IsNullOrWhiteSpace(libelle))
{
return Enumerable.Empty<Titre>();
}
return _titres
.Where(t => t.Styles.Any(s => !string.IsNullOrWhiteSpace(s.Libelle)
&& s.Libelle.Contains(libelle, StringComparison.OrdinalIgnoreCase)))
.OrderBy(t => t.Libelle)
.ToList();
}
}

View File

@@ -24,6 +24,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" /> <ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
<ProjectReference Include="..\Webzine.Repository.Contracts\Webzine.Repository.Contracts.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,63 @@
using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Recherche;
using Webzine.WebApplication.ViewModels.Titre;
namespace Webzine.WebApplication.Controllers;
[Route("recherche")]
public class RechercheController : Controller
{
private readonly ILogger<RechercheController> _logger;
private readonly ITitreRepository _titreRepository;
public RechercheController(ILogger<RechercheController> logger, ITitreRepository titreRepository)
{
_logger = logger;
_titreRepository = titreRepository;
}
[HttpPost("")]
public IActionResult Index(string mot)
{
_logger.LogInformation("Recherche artistes/titres pour le mot : {Mot}.", mot);
var titres = _titreRepository.Search(mot)
.Concat(_titreRepository.SearchByStyle(mot))
.DistinctBy(t => t.IdTitre)
.OrderBy(t => t.Libelle)
.Select(t => new TitreStyleItem
{
IdTitre = t.IdTitre,
Libelle = t.Libelle,
ArtisteNom = t.Artiste?.Nom,
UrlJaquette = t.UrlJaquette,
Duree = t.Duree
})
.ToList();
var artistes = _titreRepository.FindAll()
.Select(t => t.Artiste)
.Where(a => a != null
&& !string.IsNullOrWhiteSpace(a.Nom)
&& !string.IsNullOrWhiteSpace(mot)
&& a.Nom.Contains(mot, StringComparison.OrdinalIgnoreCase))
.DistinctBy(a => a!.IdArtiste)
.OrderBy(a => a!.Nom)
.Select(a => new RechercheArtisteItem
{
Nom = a!.Nom,
NombreDeTitres = a.Titres?.Count ?? 0
})
.ToList();
var vm = new RechercheIndexViewModel
{
Mot = mot,
Artistes = artistes,
Titres = titres
};
return View(vm);
}
}

View File

@@ -1,56 +1,45 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Webzine.Entity; using Webzine.Entity;
using Webzine.Entity.Fixtures; using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Titre; using Webzine.WebApplication.ViewModels.Titre;
namespace Webzine.WebApplication.Controllers; namespace Webzine.WebApplication.Controllers;
/// <summary> /// <summary>
/// Contrôleur responsable de la gestion des titres musicaux : /// Controleur responsable de la gestion des titres musicaux :
/// affichage des détails, filtrage par style, /// affichage des details, filtrage par style,
/// ajout de likes et commentaires. /// ajout de likes, commentaires et recherche.
/// </summary> /// </summary>
[Route("titre")] [Route("titre")]
public class TitreController : Controller public class TitreController : Controller
{ {
private readonly ILogger<TitreController> _logger; private readonly ILogger<TitreController> _logger;
private readonly List<Titre> _titres; private readonly ITitreRepository _titreRepository;
private readonly List<Style> _styles;
private readonly List<Artiste> _artistes;
/// <summary> /// <summary>
/// Initialise une nouvelle instance du <see cref="TitreController"/>. /// Initialise une nouvelle instance du <see cref="TitreController"/>.
/// Les données sont générées dynamiquement via <see cref="DataFactory"/>.
/// </summary> /// </summary>
/// <param name="logger">Service de journalisation injecté.</param> /// <param name="logger">Service de journalisation injecte.</param>
public TitreController(ILogger<TitreController> logger) /// <param name="titreRepository">Repository des titres injecte.</param>
public TitreController(ILogger<TitreController> logger, ITitreRepository titreRepository)
{ {
_logger = logger; _logger = logger;
_titreRepository = titreRepository;
_logger.LogInformation("Initialisation du contrôleur TitreController."); _logger.LogInformation("Initialisation du controleur TitreController.");
var factory = new DataFactory();
_artistes = factory.GenerateArtists(10);
_styles = factory.GenerateStyles(10);
_titres = factory.GenerateTitres(30, _artistes, _styles);
factory.GenerateCommentaires(50, _titres);
_logger.LogInformation("Données fictives générées avec succès.");
} }
/// <summary> /// <summary>
/// Affiche le détail d'un titre spécifique. /// Affiche le detail d'un titre specifique.
/// </summary> /// </summary>
/// <param name="id">Identifiant du titre.</param> /// <param name="id">Identifiant du titre.</param>
/// <returns>Vue des détails ou 404 si introuvable.</returns> /// <returns>Vue des details ou 404 si introuvable.</returns>
[HttpGet("{id}")] [HttpGet("{id}")]
public IActionResult Details(int id) public IActionResult Details(int id)
{ {
_logger.LogInformation("Demande d'affichage du détail pour le titre ID {Id}.", id); _logger.LogInformation("Demande d'affichage du detail pour le titre ID {Id}.", id);
var titre = _titres.FirstOrDefault(t => t.IdTitre == id); var titre = FindById(id);
if (titre == null) if (titre == null)
{ {
@@ -83,53 +72,37 @@ public class TitreController : Controller
} }
/// <summary> /// <summary>
/// Affiche les titres correspondant à un style musical donné. /// Affiche les titres correspondant a un style musical donne.
/// </summary> /// </summary>
/// <param name="style">Nom du style musical.</param> /// <param name="style">Nom du style musical.</param>
/// <returns>Vue contenant la liste filtrée.</returns> /// <returns>Vue contenant la liste filtree.</returns>
[HttpGet("style/{style}")] [HttpGet("style/{style}")]
public IActionResult Style(string style) public IActionResult Style(string style)
{ {
_logger.LogInformation("Recherche des titres pour le style : {Style}.", style); _logger.LogInformation("Recherche des titres pour le style : {Style}.", style);
var titresFiltres = _titres var titresFiltres = _titreRepository.SearchByStyle(style).ToList();
.Where(t => t.Styles.Any(s => s.Libelle.Equals(style)))
.OrderBy(t => t.Libelle)
.ToList();
if (!titresFiltres.Any())
{
_logger.LogWarning("Aucun titre trouvé pour le style : {Style}.", style);
return NotFound();
}
var vm = new TitreStyle var vm = new TitreStyle
{ {
StyleName = style, StyleName = style,
Titres = titresFiltres.Select(t => new TitreStyleItem Titres = titresFiltres.Select(MapTitreItem).ToList()
{
IdTitre = t.IdTitre,
Libelle = t.Libelle,
ArtisteNom = t.Artiste?.Nom,
UrlJaquette = t.UrlJaquette,
Duree = t.Duree
}).ToList()
}; };
return View(vm); return View(vm);
} }
/// <summary> /// <summary>
/// Ajoute un like à un titre. /// Ajoute un like a un titre.
/// </summary> /// </summary>
/// <param name="model">Modèle contenant l'identifiant du titre.</param> /// <param name="model">Modele contenant l'identifiant du titre.</param>
/// <returns>Redirection vers la page détail.</returns> /// <returns>Redirection vers la page detail.</returns>
[HttpPost("like")] [HttpPost("like")]
public IActionResult Like(TitreLike model) public IActionResult Like(TitreLike model)
{ {
_logger.LogInformation("Ajout d'un like pour le titre ID {Id}.", model.IdTitre); _logger.LogInformation("Ajout d'un like pour le titre ID {Id}.", model.IdTitre);
var titre = _titres.FirstOrDefault(t => t.IdTitre == model.IdTitre); var titre = FindById(model.IdTitre);
if (titre == null) if (titre == null)
{ {
@@ -143,20 +116,20 @@ public class TitreController : Controller
} }
/// <summary> /// <summary>
/// Ajoute un commentaire à un titre. /// Ajoute un commentaire a un titre.
/// </summary> /// </summary>
/// <param name="model">Données du commentaire.</param> /// <param name="model">Donnees du commentaire.</param>
/// <returns>Redirection vers la page détail.</returns> /// <returns>Redirection vers la page detail.</returns>
[HttpPost("comment")] [HttpPost("comment")]
public IActionResult Comment(TitreComment model) public IActionResult Comment(TitreComment model)
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
_logger.LogWarning("Échec de validation du modèle de commentaire pour le titre ID {Id}.", model.IdTitre); _logger.LogWarning("Echec de validation du modele de commentaire pour le titre ID {Id}.", model.IdTitre);
return RedirectToAction("Details", new { id = model.IdTitre }); return RedirectToAction("Details", new { id = model.IdTitre });
} }
var titre = _titres.FirstOrDefault(t => t.IdTitre == model.IdTitre); var titre = FindById(model.IdTitre);
if (titre == null) if (titre == null)
{ {
@@ -174,8 +147,25 @@ public class TitreController : Controller
titre.Commentaires.Add(commentaire); titre.Commentaires.Add(commentaire);
_logger.LogInformation("Commentaire ajouté avec succès au titre ID {Id}.", model.IdTitre); _logger.LogInformation("Commentaire ajoute avec succes au titre ID {Id}.", model.IdTitre);
return RedirectToAction("Details", new { id = model.IdTitre }); return RedirectToAction("Details", new { id = model.IdTitre });
} }
}
private Titre? FindById(int id)
{
return _titreRepository.Find(id);
}
private static TitreStyleItem MapTitreItem(Titre titre)
{
return new TitreStyleItem
{
IdTitre = titre.IdTitre,
Libelle = titre.Libelle,
ArtisteNom = titre.Artiste?.Nom,
UrlJaquette = titre.UrlJaquette,
Duree = titre.Duree
};
}
}

View File

@@ -1,5 +1,7 @@
using NLog; using NLog;
using NLog.Web; using NLog.Web;
using Webzine.Repository;
using Webzine.Repository.Contracts;
// Initiation du logger NLog pour la classe courante afin de pouvoir l'utiliser pour logger des messages d'information, d'erreur, etc avant la construction de l'application. // Initiation du logger NLog pour la classe courante afin de pouvoir l'utiliser pour logger des messages d'information, d'erreur, etc avant la construction de l'application.
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
@@ -9,28 +11,29 @@ try
{ {
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Ajoute les services nécessaires pour permettre l'utilisation des // Ajoute les services necessaires pour permettre l'utilisation des
// controllers avec des vues. // controllers avec des vues.
builder.Services.AddControllersWithViews() builder.Services.AddControllersWithViews()
// Ajoute la compilation des vues lors de l'exécution de l'application. // Ajoute la compilation des vues lors de l'execution de l'application.
// Cela nous évite de recompiler l'application à chaque modification de vue. // Cela nous evite de recompiler l'application a chaque modification de vue.
// Nécessite le package Nuget Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation. // Necessite le package Nuget Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.
.AddRazorRuntimeCompilation(); .AddRazorRuntimeCompilation();
builder.Services.AddSingleton<ITitreRepository, LocalEntityRepository>();
// NLog: Setup NLog for Dependency injection // NLog: Setup NLog for Dependency injection
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
builder.Host.UseNLog(); builder.Host.UseNLog();
var app = builder.Build(); var app = builder.Build();
// Active la possibilité de servir des fichiers statiques présents dans // Active la possibilite de servir des fichiers statiques presents dans
// le dossier wwwroot. // le dossier wwwroot.
app.UseStaticFiles(); app.UseStaticFiles();
// Active le middleware permettant le routage des requétes entrantes. // Active le middleware permettant le routage des requetes entrantes.
app.UseRouting(); app.UseRouting();
// Ajoute une route pour les zones (Areas) comme Admin // Ajoute une route pour les zones (Areas) comme Admin
app.MapControllerRoute( app.MapControllerRoute(
name: "areas", name: "areas",
@@ -46,7 +49,7 @@ try
} }
catch (Exception exception) catch (Exception exception)
{ {
// NLog: attrape les exceptions non gérées et les logge. // NLog: attrape les exceptions non gerees et les logge.
logger.Error(exception, "Stopped program because of exception"); logger.Error(exception, "Stopped program because of exception");
throw; throw;
} }
@@ -54,4 +57,4 @@ finally
{ {
// Assure que NLog flush tous les messages de log avant de fermer l'application. // Assure que NLog flush tous les messages de log avant de fermer l'application.
NLog.LogManager.Shutdown(); NLog.LogManager.Shutdown();
} }

View File

@@ -0,0 +1,17 @@
namespace Webzine.WebApplication.ViewModels.Recherche;
/// <summary>
/// ViewModel pour afficher un artiste dans les resultats de recherche.
/// </summary>
public class RechercheArtisteItem
{
/// <summary>
/// Nom de l'artiste.
/// </summary>
public string? Nom { get; set; }
/// <summary>
/// Nombre de titres associes a l'artiste.
/// </summary>
public int NombreDeTitres { get; set; }
}

View File

@@ -0,0 +1,24 @@
using Webzine.WebApplication.ViewModels.Titre;
namespace Webzine.WebApplication.ViewModels.Recherche;
/// <summary>
/// ViewModel pour afficher les resultats de recherche d'artistes et de titres.
/// </summary>
public class RechercheIndexViewModel
{
/// <summary>
/// Mot saisi dans le formulaire.
/// </summary>
public string? Mot { get; set; }
/// <summary>
/// Artistes trouves.
/// </summary>
public List<RechercheArtisteItem> Artistes { get; set; } = new();
/// <summary>
/// Titres trouves.
/// </summary>
public List<TitreStyleItem> Titres { get; set; } = new();
}

View File

@@ -0,0 +1,85 @@
@model Webzine.WebApplication.ViewModels.Recherche.RechercheIndexViewModel
@{
ViewData["Title"] = "Recherche";
Layout = "_Layout";
}
<div class="container mt-4">
<div class="row">
<div class="col-md-8">
<h1 class="mb-4">Resultats pour "@Model.Mot"</h1>
<hr />
@if (string.IsNullOrWhiteSpace(Model.Mot))
{
<div class="alert alert-info">
Saisissez un mot-cle pour lancer une recherche.
</div>
}
else if (!Model.Artistes.Any() && !Model.Titres.Any())
{
<div class="alert alert-info">
Aucun artiste ni titre ne correspond a votre recherche.
</div>
}
else
{
@if (Model.Artistes.Any())
{
<h2 class="h4 mt-4">Artistes</h2>
@foreach (var artiste in Model.Artistes)
{
<div class="my-3">
<a asp-controller="Artiste"
asp-action="Index"
asp-route-nom="@artiste.Nom">
@artiste.Nom
</a>
<span class="text-muted">(@artiste.NombreDeTitres titre(s))</span>
</div>
}
}
@if (Model.Titres.Any())
{
<h2 class="h4 mt-4">Titres</h2>
@foreach (var titre in Model.Titres)
{
<div class="d-flex align-items-start my-3">
<a asp-controller="Titre"
asp-action="Details"
asp-route-id="@titre.IdTitre"
class="me-3 text-black">
<img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70" height="70" class="object-fit-cover" />
</a>
<div class="justify-content-center d-flex flex-column">
<div>
<a asp-controller="Artiste"
asp-action="Index"
asp-route-nom="@titre.ArtisteNom">
@titre.ArtisteNom
</a>
-
<a asp-controller="Titre"
asp-action="Details"
asp-route-id="@titre.IdTitre">
@titre.Libelle
</a>
</div>
<div>
Duree : @TimeSpan.FromSeconds(titre.Duree).ToString(@"mm\:ss")
</div>
</div>
</div>
}
}
}
</div>
</div>
</div>

View File

@@ -3,11 +3,11 @@
*@ *@
@{ @{
} }
<div class="text-bg-light my-2 pb-0"> <div class="site-footer text-bg-light mt-auto">
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4"> <footer class="d-flex flex-wrap justify-content-between align-items-center py-3">
<div class="col-md-4 d-flex align-items-center"> <div class="col-md-4 d-flex align-items-center">
<span class="mb-3 mb-md-0 ms-5 text-body-secondary">&copy; ASP .NET Core - DIIAGE 2025 - 2026</span> <span class="mb-3 mb-md-0 ms-5 text-body-secondary">&copy; ASP .NET Core - DIIAGE 2025 - 2026</span>
</div> </div>
</footer> </footer>
</div> </div>

View File

@@ -62,10 +62,13 @@
</ul> </ul>
<!-- Barre de recherche --> <!-- Barre de recherche -->
<form class="d-flex"> <form class="d-flex" asp-controller="Recherche" asp-action="Index" method="post">
<div class="input-group"> <div class="input-group">
<div class="form-outline"> <div class="form-outline">
<input class="form-control me-2" type="search" placeholder="Trouver un artiste / titre"> <input class="form-control me-2"
type="search"
name="mot"
placeholder="Trouver un artiste, un titre ou un style">
</div> </div>
<button class="btn btn-primary" type="submit"> <button class="btn btn-primary" type="submit">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
@@ -75,4 +78,4 @@
</div> </div>
</div> </div>
</nav> </nav>
</header> </header>

View File

@@ -15,18 +15,20 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head> </head>
<body> <body>
<div class="pb-0 mb-0"> <div class="site-shell">
<partial name="_Header"/> <partial name="_Header"/>
<div class="row mt-5"> <div class="container-fluid flex-grow-1 py-4">
<main class="col mx-3"> <div class="row g-0">
@RenderBody() <main class="col mx-3">
</main> @RenderBody()
@if(ViewContext.RouteData.Values["area"]?.ToString() != "Administration") </main>
{ @if(ViewContext.RouteData.Values["area"]?.ToString() != "Administration")
<partial name="_Sidebar" /> {
} <partial name="_Sidebar" />
}
</div>
</div> </div>
<partial name="_Footer" /> <partial name="_Footer" />
</div> </div>
</body> </body>
</html> </html>