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:
@@ -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">
|
||||
© 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">
|
||||
© 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user