Merge branch 'feature/HomePage-With-Categories-their-Recipes' into 'develop'

Feature/home page with categories their recipes

See merge request francesco.damico/francescos.recipes.world!13
This commit is contained in:
Christian Hunziker
2025-04-23 09:27:22 +00:00
7 changed files with 141 additions and 4 deletions
@@ -2,6 +2,7 @@
{
using Francesco.Recipes.World.Repositories.Category;
using Francesco.Recipes.World.Repositories.Recipe;
using Francesco.Recipes.World.Views.Category;
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
@@ -19,7 +20,14 @@
public async Task<IActionResult> Index()
{
var categories = await _categoryRepository.GetAllCategoriesWithRecipesAsync();
return View("Index", categories);
var viewModel = categories.Select(category => new CategoryRecipesViewModel
{
Category = category,
Recipes = category.Recipes,
});
return View("Index", viewModel);
}
[HttpGet("/Home/Search")]
@@ -13,6 +13,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
public class RecipeController : Controller
{
private readonly IRecipeRepository _recipeRepository;
@@ -248,7 +249,13 @@
public async Task<IActionResult> AddFavorite(Guid recipeId)
{
await _favoriteRepository.AddFavoriteAsync(recipeId);
return RedirectToAction("Details", new { recipeId });
var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId);
if (recipe == null)
{
return NotFound("Recipe not found.");
}
return PartialView("_FavoriteButton", recipe);
}
// POST: /Recipe/RemoveFavorite
@@ -257,7 +264,13 @@
public async Task<IActionResult> RemoveFavorite(Guid recipeId)
{
await _favoriteRepository.RemoveFavoriteAsync(recipeId);
return RedirectToAction("Details", new { recipeId });
var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId);
if (recipe == null)
{
return NotFound("Recipe not found.");
}
return PartialView("_FavoriteButton", recipe);
}
}
}
@@ -46,6 +46,7 @@
{
return await _context.Categories
.Include(c => c.Recipes)
.ThenInclude(r => r.MediaFiles)
.ToListAsync();
}
}
@@ -1,6 +1,86 @@
<form hx-get="/Home/Search" hx-target="#search-results" hx-trigger="keyup changed delay:300ms" onsubmit="return false;" class="mb-4">
@model IEnumerable<Francesco.Recipes.World.Views.Category.CategoryRecipesViewModel>
@Html.AntiForgeryToken()
<div class="welcome-banner text-center mb-4">
<img src="/images/banner2.jpg" alt="Willkommen" class="img-fluid" />
<h1 class="mt-3">Willkommen in der Rezept-App</h1>
</div>
<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>
@foreach (var category in Model)
{
<div class="category-section mb-5" id="category-@category.Category.Id">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>@category.Category.Name</h2>
<a href="/Category/Details/@category.Category.Id"
class="btn btn-link"
hx-get="/Category/Details/@category.Category.Id"
hx-target="#category-@category.Category.Id"
hx-swap="outerHTML">Alle @category.Category.Name-Rezepte anzeigen</a>
</div>
<div class="row">
@foreach (var recipe in category.Recipes)
{
var mediaFile = recipe.MediaFiles?.FirstOrDefault();
var imageData = mediaFile?.Data;
var mimeType = mediaFile?.MimeType;
<div class="col-md-3 mb-4">
<a href="/Recipe/Details/@recipe.Id" class="text-decoration-none">
<div class="card h-100">
<div class="recipe-image">
@if (imageData != null && mimeType != null)
{
<img src="data:@mimeType;base64,@Convert.ToBase64String(imageData)" alt="@recipe.Name" class="card-img-top" />
}
else
{
<img src="/images/placeholder.png" alt="@recipe.Name" class="card-img-top" />
}
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title">@recipe.Name</h5>
<p class="card-text text-muted mb-2">@recipe.CookingTime</p>
<div id="favorite-button-@recipe.Id">
@await Html.PartialAsync("_FavoriteButton", recipe)
</div>
<a href="/Recipe/Details/@recipe.Id"
class="btn btn-primary mt-auto">Details</a>
</div>
</div>
</a>
</div>
}
<div class="col-md-3 mb-4">
<div class="card h-100 text-center">
<div class="card-body d-flex flex-column justify-content-center">
<a href="/Recipe/Create?categoryId=@category.Category.Id"
class="btn btn-outline-primary"
hx-get="/Recipe/Create?categoryId=@category.Category.Id"
hx-target="#category-@category.Category.Id"
hx-swap="beforeend">Rezept hinzufügen</a>
</div>
</div>
</div>
</div>
</div>
}
@@ -0,0 +1,25 @@
@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe
<button class="btn btn-link p-0"
style="width: 40px; height: 40px;"
hx-post="@Url.Action(Model.IsFavorite ? "RemoveFavorite" : "AddFavorite", "Recipe")"
hx-vals='{"recipeId": "@Model.Id"}'
hx-target="this"
hx-swap="outerHTML">
@if (Model.IsFavorite)
{
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFD700">
<path d="M12 17.27L18.18 21 16.54 13.97
22 9.24l-7.19-.62L12 2 9.19 8.62
2 9.24l5.46 4.73L5.82 21z" />
</svg>
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 17.27L18.18 21 16.54 13.97
22 9.24l-7.19-.62L12 2 9.19 8.62
2 9.24l5.46 4.73L5.82 21z" />
</svg>
}
</button>
@@ -73,6 +73,15 @@
document.body.classList.remove('bg-dark', 'text-white');
});
</script>
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
if (token) {
event.detail.headers['RequestVerificationToken'] = token;
}
});
</script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"/></svg>

After

Width:  |  Height:  |  Size: 297 B