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 RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId);
|
||||||
|
|
||||||
Task<IEnumerable<Recipe>> GetRecipesByNameAndIngredientAsync(string name, string ingredient);
|
|
||||||
|
|
||||||
Task<IReadOnlyCollection<Recipe>> GetRecipesByDifficultyAsync(Difficulty? difficulty);
|
Task<IReadOnlyCollection<Recipe>> GetRecipesByDifficultyAsync(Difficulty? difficulty);
|
||||||
|
|
||||||
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
|
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();
|
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)
|
.Include(r => r.RecipeIngredients)
|
||||||
.ThenInclude(ri => ri.Ingredient)
|
.ThenInclude(ri => ri.Ingredient)
|
||||||
.AsQueryable();
|
.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))
|
return await queryable.ToListAsync();
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyCollection<Recipe>> GetRecipesByDifficultyAsync(Difficulty? difficulty)
|
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -20,12 +20,29 @@
|
|||||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||||
<ul class="navbar-nav flex-grow-1">
|
<ul class="navbar-nav flex-grow-1">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
|
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Startseite</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
<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>
|
</li>
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -45,6 +62,18 @@
|
|||||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.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="~/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>
|
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" integrity="sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" crossorigin="anonymous"></script>
|
||||||
|
<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)
|
@await RenderSectionAsync("Scripts", required: false)
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user