Merge branch 'feature/filter-Recipe-and-Ingredient' into 'develop'

filter-Recipe-and-Ingredient

See merge request francesco.damico/francescos.recipes.world!10
This commit is contained in:
Dömu Brunner
2025-04-17 09:43:31 +00:00
6 changed files with 176 additions and 60 deletions
@@ -0,0 +1,32 @@
namespace Francesco.Recipes.World.Controller.HomeController
{
using Francesco.Recipes.World.Repositories.Category;
using Francesco.Recipes.World.Repositories.Recipe;
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
private readonly IRecipeRepository _recipeRepository;
private readonly ICategoryRepository _categoryRepository;
public HomeController(ICategoryRepository categoryRepository, IRecipeRepository recipeRepository)
{
_recipeRepository = recipeRepository;
_categoryRepository = categoryRepository;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var categories = await _categoryRepository.GetAllCategoriesWithRecipesAsync();
return View("Index", categories);
}
[HttpGet("/Home/Search")]
public async Task<IActionResult> Search(string term)
{
var recipes = await _recipeRepository.SearchInRecipesAndIngredients(term);
return PartialView("_SearchResultsPartial", recipes);
}
}
}
@@ -13,10 +13,10 @@
Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId);
Task<IEnumerable<Recipe>> GetRecipesByNameAndIngredientAsync(string name, string ingredient);
Task<IReadOnlyCollection<Recipe>> GetRecipesByDifficultyAsync(Difficulty? difficulty);
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
Task<IEnumerable<Recipe>> SearchInRecipesAndIngredients(string searchterm);
}
}
@@ -147,30 +147,21 @@
await _context.SaveChangesAsync();
}
public async Task<IEnumerable<Recipe>> GetRecipesByNameAndIngredientAsync(string name, string ingredient)
public async Task<IEnumerable<Recipe>> SearchInRecipesAndIngredients(string searchTerm)
{
var query = _context.Recipes
var queryable = _context.Recipes
.Include(r => r.RecipeIngredients)
.ThenInclude(ri => ri.Ingredient)
.ThenInclude(ri => ri.Ingredient)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(name))
if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(r => r.Name.Contains(name, StringComparison.OrdinalIgnoreCase));
searchTerm = searchTerm.ToLower();
queryable = queryable.Where(r => r.Name.ToLower().Contains(searchTerm) ||
r.RecipeIngredients.Any(ri => ri.Ingredient.Name.ToLower().Contains(searchTerm)));
}
if (!string.IsNullOrWhiteSpace(ingredient))
{
var ingredientMatches = await _ingredientRepository.GetIngredientsByNameAsync(ingredient);
var ingredientIds = ingredientMatches.Select(i => i.Id).ToList();
if (ingredientIds.Any())
{
query = query.Where(r => r.RecipeIngredients.Any(ri => ingredientIds.Contains(ri.Ingredient.Id)));
}
}
return await query.ToListAsync();
return await queryable.ToListAsync();
}
public async Task<IReadOnlyCollection<Recipe>> GetRecipesByDifficultyAsync(Difficulty? difficulty)
@@ -0,0 +1,6 @@
<form hx-get="/Home/Search" hx-target="#search-results" hx-trigger="keyup changed delay:300ms" onsubmit="return false;" class="mb-4">
<input type="text" name="query" class="form-control" placeholder="Suchen nach Rezepten oder Zutaten..." autocomplete="off" />
</form>
<div id="search-results" class="mt-4"></div>
@@ -0,0 +1,58 @@
@model IEnumerable<Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe>
@{
if (!Model.Any())
{
<p class="text-muted">Keine Ergebnisse gefunden.</p>
}
else
{
<div class="row">
@foreach (var recipe in Model)
{
<div class="col-md-4 mb-3">
<div class="card h-100 shadow-sm">
<div class="recipe-image">
@{
var mediaFile = recipe.MediaFiles.FirstOrDefault();
if (mediaFile?.Data != null)
{
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)"
alt="@recipe.Name"
class="card-img-top" />
}
else
{
<img src="~/images/placeholder.png"
alt="Platzhalter"
class="card-img-top" />
}
}
</div>
<div class="card-body d-flex flex-column justify-content-between">
<div>
<h5 class="card-title">@recipe.Name</h5>
<p class="card-text text-muted mb-2">
<i class="bi bi-clock"></i>
@recipe.PreparationTime.Hours h @recipe.PreparationTime.Minutes min
</p>
</div>
<div>
<button class="btn btn-outline-secondary"
hx-post="/Recipe/ToggleFavorite"
hx-vals='{"id": "@recipe.Id"}'
hx-swap="outerHTML">
<i class="bi @((recipe.IsFavorite) ? "bi-star-fill" : "bi-star")"></i>
</button>
</div>
</div>
</div>
</div>
}
</div>
}
}
@@ -1,50 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Francesco.Recipes.World</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/Francesco.Recipes.World.styles.css" asp-append-version="true" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Francesco.Recipes.World</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/Francesco.Recipes.World.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Francesco.Recipes.World</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Francesco.Recipes.World</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Startseite</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Recipes" asp-action="Index">Rezepte</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Category" asp-action="Index">Kategorien</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="ShoppingList" asp-action="Index">Einkaufsliste</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Favorites" asp-action="Index">Favoriten</a>
</li>
</ul>
<div class="d-flex">
<button class="btn btn-outline-dark me-2" id="darkModeToggle">
<i class="bi bi-moon"></i>
</button>
<button class="btn btn-outline-light" id="lightModeToggle">
<i class="bi bi-sun"></i>
</button>
</div>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2024 - Francesco.Recipes.World - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2024 - Francesco.Recipes.World - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" integrity="sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" crossorigin="anonymous"></script>
@await RenderSectionAsync("Scripts", required: false)
<script>
// Light/Dark mode toggle logic
const darkModeToggle = document.getElementById('darkModeToggle');
const lightModeToggle = document.getElementById('lightModeToggle');
darkModeToggle.addEventListener('click', () => {
document.body.classList.add('bg-dark', 'text-white');
});
lightModeToggle.addEventListener('click', () => {
document.body.classList.remove('bg-dark', 'text-white');
});
</script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>