Finished Project

This commit is contained in:
Francesco Lorenzo D'Amico
2026-03-02 16:35:32 +01:00
parent 247fbd6ae9
commit b546d8946e
21 changed files with 333 additions and 205 deletions
@@ -50,5 +50,4 @@
<Folder Include="Services\MediaFile\" />
<Folder Include="Services\Ingredient\" />
</ItemGroup>
</Project>
@@ -4,8 +4,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models;
using Francesco.Recipes.World.Models.BackendModels.Category;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Views.Category;
using Microsoft.EntityFrameworkCore;
public class CategoryRepository : ICategoryRepository
@@ -44,11 +46,44 @@
public async Task<IEnumerable<Category>> GetAllCategoriesWithRecipesAsync()
{
return await _context.Categories
.Include(c => c.Recipes)
.ThenInclude(r => r.MediaFiles)
.AsSplitQuery()
var categories = await _context.Categories
.Include(c => c.Recipes.OrderByDescending(r => r.CreatedAt).Take(3))
.ThenInclude(r => r.MediaFiles.Take(2))
.AsSplitQuery()
.ToListAsync();
return categories;
}
public async Task<IEnumerable<CategoryRecipesViewModel>> GetAllCategoriesWithRecipesViewModelAsync()
{
var categories = await _context.Categories
.Select(c => new CategoryRecipesViewModel
{
Category = c,
Recipes = c.Recipes
.OrderByDescending(r => r.CreatedAt)
.Take(3)
.Select(r => new RecipeCardViewModel
{
Id = r.Id,
Name = r.Name,
CookingTime = r.CookingTime,
IsFavorite = r.IsFavorite,
ImageData = r.MediaFiles
.OrderBy(m => m.Id)
.Select(m => m.Data)
.FirstOrDefault(),
MimeType = r.MediaFiles
.OrderBy(m => m.Id)
.Select(m => m.MimeType)
.FirstOrDefault(),
}),
})
.AsSplitQuery()
.ToListAsync();
return categories;
}
}
}
@@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Francesco.Recipes.World.Models.BackendModels.Category;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Views.Category;
public interface ICategoryRepository
{
@@ -15,5 +16,7 @@
Task<IEnumerable<Recipe>> GetRecipesByCategoryAsync(Guid categoryId);
Task<IEnumerable<Category>> GetAllCategoriesWithRecipesAsync();
Task<IEnumerable<CategoryRecipesViewModel>> GetAllCategoriesWithRecipesViewModelAsync();
}
}
@@ -19,6 +19,7 @@
.Where(r => r.IsFavorite)
.Include(r => r.Favorite)
.Include(r => r.MediaFiles)
.Take(6)
.ToListAsync();
}
@@ -19,6 +19,7 @@
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
Task<bool> DeleteRecipeAsync(Guid recipeId);
Task<IEnumerable<SearchViewModel>> SearchInRecipesAndIngredients(string searchTerm);
}
}
@@ -198,35 +198,6 @@
.ToListAsync();
}
private static Expression<Func<Recipe, SearchViewModel>> SearchViewModelSelector()
{
return r => new SearchViewModel
{
Id = r.Id,
Name = r.Name,
Description = r.Description,
IsFavorite = r.IsFavorite,
ImageData = r.MediaFiles
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
.Select(m => m.Data)
.FirstOrDefault(),
MimeType = r.MediaFiles
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
.Select(m => m.MimeType)
.FirstOrDefault(),
Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(),
TotalTime = r.PreparationTime.Add(r.CookingTime),
};
}
private static IQueryable<Recipe> ApplyRecipeSearchFilter(IQueryable<Recipe> query, string normalizedSearchTerm)
{
return query.Where(r =>
EF.Functions.Like(r.Name.ToLower(), $"%{normalizedSearchTerm}%") ||
(r.Description != null && EF.Functions.Like(r.Description.ToLower(), $"%{normalizedSearchTerm}%")) ||
r.RecipeIngredients.Any(ri => EF.Functions.Like(ri.Ingredient.Name.ToLower(), $"%{normalizedSearchTerm}%")));
}
public async Task<bool> DeleteRecipeAsync(Guid recipeId)
{
var recipe = await GetRecipeByIdAsync(recipeId);
@@ -259,9 +230,9 @@
_context.MediaFiles.RemoveRange(recipe.MediaFiles);
}
if (recipe.Favorit != null && recipe.Favorit.Id != Guid.Empty)
if (recipe.Favorite != null && recipe.Favorite.Id != Guid.Empty)
{
_context.Remove(recipe.Favorit);
_context.Remove(recipe.Favorite);
}
_context.Recipes.Remove(recipe);
@@ -269,5 +240,34 @@
await _context.SaveChangesAsync();
return true;
}
private static Expression<Func<Recipe, SearchViewModel>> SearchViewModelSelector()
{
return r => new SearchViewModel
{
Id = r.Id,
Name = r.Name,
Description = r.Description,
IsFavorite = r.IsFavorite,
ImageData = r.MediaFiles
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
.Select(m => m.Data)
.FirstOrDefault(),
MimeType = r.MediaFiles
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
.Select(m => m.MimeType)
.FirstOrDefault(),
Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(),
TotalTime = r.PreparationTime.Add(r.CookingTime),
};
}
private static IQueryable<Recipe> ApplyRecipeSearchFilter(IQueryable<Recipe> query, string normalizedSearchTerm)
{
return query.Where(r =>
EF.Functions.Like(r.Name.ToLower(), $"%{normalizedSearchTerm}%") ||
(r.Description != null && EF.Functions.Like(r.Description.ToLower(), $"%{normalizedSearchTerm}%")) ||
r.RecipeIngredients.Any(ri => EF.Functions.Like(ri.Ingredient.Name.ToLower(), $"%{normalizedSearchTerm}%")));
}
}
}
@@ -1,12 +1,12 @@
namespace Francesco.Recipes.World.Views.Category
{
using Francesco.Recipes.World.Models;
using Francesco.Recipes.World.Models.BackendModels.Category;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
public class CategoryRecipesViewModel
{
public Category Category { get; set; } = new ();
public IEnumerable<Recipe> Recipes { get; set; } = new List<Recipe>();
public IEnumerable<RecipeCardViewModel> Recipes { get; set; } = new List<RecipeCardViewModel>();
}
}
+30 -28
View File
@@ -32,39 +32,41 @@
</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">
<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" />
}
@foreach (var recipe in category.Recipes)
{
var imageData = recipe.ImageData;
var mimeType = recipe.MimeType;
<div class="col-md-3 mb-4">
<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>
<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="/Details/@recipe.Id"
class="btn btn-primary mt-auto">Details</a>
</div>
</div>
</div>
}
<a href="/Details/@recipe.Id"
class="btn btn-primary mt-auto">Details</a>
</div>
</div>
</div>
}
<div class="col-md-3 mb-4">
<div class="card h-100 text-center">
@@ -64,7 +64,7 @@
</form>
</div>
<a href="/Recipe/Details/@recipe.Id" class="btn btn-primary btn-sm">Details</a>
<a href="/Details/@recipe.Id" class="btn btn-primary btn-sm">Details</a>
</div>
</div>
</div>
@@ -1,98 +0,0 @@
@model IEnumerable<Francesco.Recipes.World.Views.Category.CategoryRecipesViewModel>
@{
ViewData["Title"] = "Category Recipes";
}
<h1>Category Recipes</h1>
@foreach (var categoryRecipes in Model)
{
<div class="category-section">
<h2>@categoryRecipes.Category.Name</h2>
<a asp-action="Create" asp-route-categoryId="@categoryRecipes.Category.Id" class="btn btn-primary">Rezept erstellen</a>
<div class="recipes">
@foreach (var recipe in categoryRecipes.Recipes)
{
<div class="recipe-card">
<div class="recipe-image">
@if (recipe.MediaFiles.Any() && recipe.MediaFiles.First().Data != null)
{
var mediaFile = recipe.MediaFiles.First();
if (mediaFile.Data != null)
{
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)" alt="@recipe.Name" class="img-fluid" />
}
}
</div>
<div class="recipe-info">
<h3>@recipe.Name</h3>
<p>@recipe.Description</p>
<p><strong>Difficulty:</strong> @recipe.Difficulty</p>
<p><strong>Servings:</strong> @recipe.Servings</p>
<p><strong>Preparation Time:</strong> @recipe.PreparationTime</p>
<p><strong>Cooking Time:</strong> @recipe.CookingTime</p>
<div class="recipe-favorite">
@if (recipe.IsFavorite)
{
<form method="post" asp-action="RemoveFavorite" asp-controller="Recipe">
<input type="hidden" name="recipeId" value="@recipe.Id" />
<button type="submit" class="btn btn-danger">Remove from Favorites</button>
</form>
}
else
{
<form method="post" asp-action="AddFavorite" asp-controller="Recipe">
<input type="hidden" name="recipeId" value="@recipe.Id" />
<button type="submit" class="btn btn-primary">Add to Favorites</button>
</form>
}
</div>
</div>
</div>
}
<div class="recipe-card add-recipe-card">
<a asp-action="Create" asp-route-categoryId="@categoryRecipes.Category.Id" class="btn btn-primary">Rezept hinzufügen</a>
</div>
</div>
</div>
}
<style>
.category-section {
margin-bottom: 2rem;
}
.recipes {
display: flex;
flex-wrap: wrap;
}
.recipe-card {
width: 200px;
margin: 1rem;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.recipe-image img {
width: 100%;
height: auto;
border-radius: 8px;
}
.recipe-info {
margin-top: 1rem;
}
.add-recipe-card {
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f8f8;
border: 2px dashed #ccc;
}
</style>
@@ -107,7 +107,7 @@
<div class="text-center mt-4">
<button type="submit" class="btn btn-success btn-lg">Rezept speichern</button>
<a asp-action="Index" asp-controller="Category" class="btn btn-secondary btn-lg ml-2">Abbrechen</a>
<a asp-action="Index" asp-controller="Home" class="btn btn-secondary btn-lg ml-2">Abbrechen</a>
</div>
</form>
@@ -32,7 +32,7 @@
Instructions = Model.Instructions.ToList()
})
<form hx-post="@($"/{Model.Id}/Delete")"
<form hx-delete="@($"/{Model.Id}/Delete")"
hx-confirm="Sind Sie sicher, dass Sie dieses Rezept löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
hx-redirect="/">
@Html.AntiForgeryToken()
@@ -173,34 +173,37 @@
return Math.round(quantity * 10) / 10;
}
window.addSelectedIngredientsToShoppingList = function(recipeIdParam) {
const idToUse = recipeIdParam || recipeId;
window.addSelectedIngredientsToShoppingList = function(recipeIdParam) {
const idToUse = recipeIdParam || recipeId;
var form = document.getElementById('ingredient-form');
var formData = new FormData(form);
var selectedIngredientIds = formData.getAll('ingredientIds');
var form = document.getElementById('ingredient-form');
var formData = new FormData(form);
var selectedIngredientIds = formData.getAll('ingredientIds');
fetch('/ShoppingList/CreateOrAddIngredients', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
},
body: JSON.stringify({ recipeId: idToUse, ingredientIds: selectedIngredientIds })
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Failed to update shopping list');
})
.then(result => {
localStorage.setItem('shoppingListId', result.shoppingListId);
alert('Einkaufsliste aktualisiert.');
})
.catch(error => {
alert('Fehler beim Aktualisieren der Einkaufsliste.');
});
};
});
var token = document.querySelector('input[name="__RequestVerificationToken"]').value;
var response = await fetch('/ShoppingList/CreateOrAddIngredients', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token
},
body: JSON.stringify({ recipeId: '@Model.Id', ingredientIds: selectedIngredientIds })
});
if (response.ok) {
var result = await response.json();
localStorage.setItem('shoppingListId', result.shoppingListId);
alert('Zutaten wurden zur Einkaufsliste hinzugefügt.');
} else {
alert('Fehler beim Hinzufügen zur Einkaufsliste.');
}
}
</script>
}
@@ -1,4 +1,4 @@
@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe
@model Francesco.Recipes.World.Models.IFavoritable
<form hx-post="@Url.Action(Model.IsFavorite ? "RemoveFavorite" : "AddFavorite", "Recipe")"
hx-target="this"
@@ -36,3 +36,4 @@
<script src="~/js/site.js" asp-append-version="true" defer></script>
@@ -21,13 +21,9 @@
}
</select>
<button type="button" class="btn-delete" onclick="Francesco.removeIngredient('@Model.Ingredients[i].Id')">🗑️</button>
</div>
</div>
}
</div>
<button type="button" class="btn-add" onclick="Francesco.addIngredient()">Zutat hinzufügen</button>
@@ -23,17 +23,12 @@
<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>
<a class="nav-link text-dark" asp-area="" asp-controller="ShoppingList" asp-action="Details">Einkaufsliste</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>
<a class="nav-link text-dark" asp-area="" asp-controller="Favorite" asp-action="Index">Favoriten</a>
</li>
</ul>
<div class="d-flex">
@@ -413,3 +413,4 @@
};
})(window, document);
console.log("Francesco-Objekt initialisiert:", window.Francesco);