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