From ff08b8261159639b516e55e49ad51c1be3ebd73c Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 13 May 2025 14:44:18 +0200 Subject: [PATCH 01/40] 1.) Add an eindpoint for deleting recipe with their connections 2.) Add Endpoint for adjust Ingredient Amount --- .../Controller/Recipe/RecipeController.cs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index e19eea3..551e8fa 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -412,7 +412,7 @@ } catch (Exception ex) { - return BadRequest(ex.Message); + return BadRequest(ex.Message); } } @@ -470,5 +470,49 @@ return PartialView("_IngredientsPartial", viewModel); } + + // GET: /Recipe/{recipeId}/AdjustableIngredients + [HttpGet("{recipeId}/AdjustableIngredients")] + public async Task GetAdjustableIngredients(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return PartialView("_AdjustableIngredientsPartial", recipe); + } + + // GET: /Recipe/{recipeId}/Delete + [HttpGet("{recipeId}/Delete")] + public async Task Delete(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return View(recipe); + } + + // POST: /Recipe/{recipeId}/Delete + [HttpPost("{recipeId}/Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(Guid recipeId) + { + try + { + await _recipeRepository.DeleteRecipeAsync(recipeId); + TempData["SuccessMessage"] = "Rezept wurde erfolgreich gelöscht."; + return RedirectToAction("CategoryRecipes", "Recipe"); + } + catch (Exception ex) + { + TempData["ErrorMessage"] = $"Ein Fehler ist aufgetreten: {ex.Message}"; + return RedirectToAction("Details", new { recipeId }); + } + } } } From 1c47b770b421f787d651f400f027e7d4e57cf5dc Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 13 May 2025 14:45:39 +0200 Subject: [PATCH 02/40] Add Method to delete whole recipe with their relations and include Mediafiles to Instruction by getting recipe --- .../Repositories/Recipe/IRecipeRepository.cs | 2 + .../Repositories/Recipe/RecipeRepository.cs | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index fdbc876..2e4b375 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -18,5 +18,7 @@ Task CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime); Task> SearchInRecipesAndIngredients(string searchterm); + + Task DeleteRecipeAsync(Guid recipeId); } } diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index 382bf90..4492d1e 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -38,6 +38,7 @@ .ThenInclude(ri => ri.Unit) .Include(r => r.MediaFiles) .Include(r => r.Instructions) + .ThenInclude(i => i.MediaFiles) .FirstOrDefaultAsync(r => r.Id == recipeId); } @@ -182,5 +183,47 @@ .ThenInclude(ri => ri.Ingredient) .ToListAsync(); } + + public async Task DeleteRecipeAsync(Guid recipeId) + { + var recipe = await GetRecipeByIdAsync(recipeId); + + if (recipe == null) + { + throw new InvalidDataException($"Rezept mit ID {recipeId} nicht gefunden."); + } + + if (recipe.RecipeIngredients != null && recipe.RecipeIngredients.Any()) + { + _context.RecipeIngredients.RemoveRange(recipe.RecipeIngredients); + } + + if (recipe.Instructions != null && recipe.Instructions.Any()) + { + foreach (var instruction in recipe.Instructions) + { + if (instruction.MediaFiles != null && instruction.MediaFiles.Any()) + { + _context.MediaFiles.RemoveRange(instruction.MediaFiles); + } + } + + _context.Instructions.RemoveRange(recipe.Instructions); + } + + if (recipe.MediaFiles != null && recipe.MediaFiles.Any()) + { + _context.MediaFiles.RemoveRange(recipe.MediaFiles); + } + + if (recipe.Favorit != null && recipe.Favorit.Id != Guid.Empty) + { + _context.Remove(recipe.Favorit); + } + + _context.Recipes.Remove(recipe); + + await _context.SaveChangesAsync(); + } } } From a57ebd450757be43f24d1e77c17f745f4f1b7eb3 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 13 May 2025 14:46:59 +0200 Subject: [PATCH 03/40] Adjust endpoint path for navigation --- Francesco.Recipes.World/Views/Home/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Views/Home/Index.cshtml b/Francesco.Recipes.World/Views/Home/Index.cshtml index af2e530..0c9e60c 100644 --- a/Francesco.Recipes.World/Views/Home/Index.cshtml +++ b/Francesco.Recipes.World/Views/Home/Index.cshtml @@ -60,7 +60,7 @@ @await Html.PartialAsync("_FavoriteButton", recipe) - Details From 325120e5ecfe517002ef6295196e558f0ccfeb6d Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 13 May 2025 14:52:16 +0200 Subject: [PATCH 04/40] Add style to Instructions Grid Layout --- Francesco.Recipes.World/wwwroot/css/site.css | 100 ++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/wwwroot/css/site.css b/Francesco.Recipes.World/wwwroot/css/site.css index a505610..b8d4e75 100644 --- a/Francesco.Recipes.World/wwwroot/css/site.css +++ b/Francesco.Recipes.World/wwwroot/css/site.css @@ -84,4 +84,102 @@ textarea.form-control { border-radius: 4px; cursor: pointer; margin-top: 10px; -} \ No newline at end of file +} + + +/* Recipe Instructions Grid Layout */ +.instructions-grid { + margin-top: 2rem; + margin-bottom: 2rem; +} + +.instruction-row { + display: flex; + justify-content: space-between; + margin-bottom: 2.5rem; + flex-wrap: wrap; +} + +.instruction-card { + width: calc(33.333% - 20px); + margin-bottom: 1.5rem; +} + +@media (max-width: 992px) { + .instruction-card { + width: calc(50% - 15px); + } +} + +@media (max-width: 576px) { + .instruction-card { + width: 100%; + } +} + +.instruction-image { + position: relative; + width: 100%; + aspect-ratio: 1/1; + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + background-color: #f8f9fa; +} + + .instruction-image img { + width: 100%; + height: 100%; + object-fit: cover; + } + +.placeholder-image { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #aaa; +} + + .placeholder-image i { + font-size: 2rem; + } + +.step-number { + position: absolute; + bottom: 5px; + right: 10px; + background-color: rgba(0, 0, 0, 0.5); + color: white; + font-weight: bold; + padding: 3px 8px; + border-radius: 50%; +} + +.instruction-text { + font-size: 0.9rem; + line-height: 1.4; + color: #333; +} + +.recipe-instructions-section h3 { + margin-bottom: 1.5rem; + position: relative; + padding-bottom: 0.5rem; +} + + .recipe-instructions-section h3:after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 50px; + height: 2px; + background-color: #007bff; + } + From aa6f27383f4071f06e08ba17943275feb905d7b5 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 14 May 2025 11:46:48 +0200 Subject: [PATCH 05/40] Move the Ingredient List stuff to a Partial view --- .../Views/Recipe/Details.cshtml | 204 +++++++++++++++--- 1 file changed, 171 insertions(+), 33 deletions(-) diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml index 4ea25c7..e30b936 100644 --- a/Francesco.Recipes.World/Views/Recipe/Details.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Details.cshtml @@ -6,7 +6,7 @@

@Model.Name

-
+
@if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null) { @@ -20,49 +20,187 @@

Description: @Model.Description

Difficulty: @Model.Difficulty

-

Servings: @Model.Servings

Preparation Time: @Model.PreparationTime

Cooking Time: @Model.CookingTime

-
-

Ingredients

-
-
    - @foreach (var ingredient in Model.RecipeIngredients) - { -
  • - - @ingredient.Ingredient.Name - @ingredient.Quantity @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty) -
  • - } -
- -
-
+ + + + @await Html.PartialAsync("_RecipeInstructionGridPartial", new Francesco.Recipes.World.Models.InstructionViewModel +{ + RecipeId = Model.Id, + Instructions = Model.Instructions.ToList() +}) + +
+ @Html.AntiForgeryToken() + +
@section Scripts { } From bfbfd34624c2401b001db06ae1b624ed99bb361d Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 14 May 2025 11:47:29 +0200 Subject: [PATCH 06/40] Create Grid for show instructions of recipe --- .../_RecipeInstructionGridPartial.cshtml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml diff --git a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml new file mode 100644 index 0000000..2a7b44b --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml @@ -0,0 +1,43 @@ +@model Francesco.Recipes.World.Models.InstructionViewModel + +
+

Instructions

+ +
+ @{ + var groupedInstructions = Model.Instructions.OrderBy(i => i.Number).ToList(); + var rows = (int)Math.Ceiling(groupedInstructions.Count / 3.0); + + for (int row = 0; row < rows; row++) + { +
+ @for (int col = 0; col < 3 && (row * 3 + col < groupedInstructions.Count); col++) + { + var instruction = groupedInstructions[row * 3 + col]; +
+
+ @if (instruction.MediaFiles != null && instruction.MediaFiles.Any()) + { + var mediaFile = instruction.MediaFiles.First(); + @if (mediaFile.Data != null && mediaFile.Data.Length > 0) + { + Step @instruction.Number + } + } + + else + { +
+ +
+ } + @instruction.Number +
+

@instruction.Description

+
+ } +
+ } + } +
+
From e470dd11bbc252e3e76b264c93c1be29f33e1f79 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 14 May 2025 11:57:03 +0200 Subject: [PATCH 07/40] Make a partial view for adjust amount of ingredient --- .../_AdjustableIngredientsPartial.cshtml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml diff --git a/Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml new file mode 100644 index 0000000..7855058 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml @@ -0,0 +1,36 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe + +
+
+

Ingredients

+
+ +
+ + + +
+ (Original: @Model.Servings) +
+
+ +
+
    + @foreach (var ingredient in Model.RecipeIngredients) + { +
  • + + @ingredient.Ingredient.Name - + @ingredient.Quantity + @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty) + +
  • + } +
+ +
+
From 144328423626813657714921799ee43a5bc2671c Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 14 May 2025 12:00:54 +0200 Subject: [PATCH 08/40] Add bootastrap for icon by darkMode and LightMode --- Francesco.Recipes.World/Views/Shared/_Layout.cshtml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml index a06f9bd..8eae480 100644 --- a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml @@ -7,6 +7,8 @@ + +
From da2fd1e61d2f424b0cdeeb3f2842b469af599ab4 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 14 May 2025 12:01:20 +0200 Subject: [PATCH 09/40] change redirection to index --- Francesco.Recipes.World/Controller/Recipe/RecipeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 551e8fa..ae4e79f 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -506,7 +506,7 @@ { await _recipeRepository.DeleteRecipeAsync(recipeId); TempData["SuccessMessage"] = "Rezept wurde erfolgreich gelöscht."; - return RedirectToAction("CategoryRecipes", "Recipe"); + return RedirectToAction("Index", "Home"); } catch (Exception ex) { From a5b70532e6ae4c5092aa87e784cb51fb244b130a Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 14 May 2025 12:01:31 +0200 Subject: [PATCH 10/40] rest --- Francesco.Recipes.World/wwwroot/js/site.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Francesco.Recipes.World/wwwroot/js/site.js b/Francesco.Recipes.World/wwwroot/js/site.js index 12bdab8..20666ef 100644 --- a/Francesco.Recipes.World/wwwroot/js/site.js +++ b/Francesco.Recipes.World/wwwroot/js/site.js @@ -189,3 +189,25 @@ async function addIngredient() { } } + + async function addSelectedIngredientsToShoppingList() { + var form = document.getElementById('ingredient-form'); + var formData = new FormData(form); + var selectedIngredientIds = formData.getAll('ingredientIds'); + + var response = await fetch('/ShoppingList/CreateOrAddIngredients', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ recipeId: '@Model.Id', ingredientIds: selectedIngredientIds }) + }); + + if (response.ok) { + var result = await response.json(); + localStorage.setItem('shoppingListId', result.shoppingListId); + alert('Shopping list updated.'); + } else { + alert('Failed to update shopping list.'); + } +} \ No newline at end of file From 3fe194886740e51e5de68e9a1d4a5fabb4ffa725 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 14 May 2025 12:01:59 +0200 Subject: [PATCH 11/40] remove Service Folder --- Francesco.Recipes.World/Francesco.Recipes.World.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Francesco.Recipes.World/Francesco.Recipes.World.csproj b/Francesco.Recipes.World/Francesco.Recipes.World.csproj index 84f6487..e28537c 100644 --- a/Francesco.Recipes.World/Francesco.Recipes.World.csproj +++ b/Francesco.Recipes.World/Francesco.Recipes.World.csproj @@ -49,7 +49,6 @@ - From 8d9eb4b364e10c336a60d49e45eb4f87194e0831 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 20 May 2025 14:22:28 +0200 Subject: [PATCH 12/40] Everywhere an delete operation is name from HttpPost to HttpDelete --- .../Controller/Recipe/RecipeController.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index ae4e79f..9d31328 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -305,8 +305,8 @@ return View(); } - // POST: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} - [HttpPost("{recipeId}/RemoveIngredient/{ingredientId}")] + // DELETE: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} + [HttpDelete("{recipeId}/RemoveIngredient/{ingredientId}")] [ValidateAntiForgeryToken] public async Task RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId) { @@ -338,8 +338,8 @@ return View(); } - // POST: /Recipe/{recipeId}/RemoveInstruction/{instructionId} - [HttpPost("{recipeId}/RemoveInstruction/{instructionId}")] + // DELETE: /Recipe/{recipeId}/RemoveInstruction/{instructionId} + [HttpDelete("{recipeId}/RemoveInstruction/{instructionId}")] [ValidateAntiForgeryToken] public async Task RemoveInstructionConfirmed(Guid recipeId, Guid instructionId) { @@ -498,19 +498,20 @@ } // POST: /Recipe/{recipeId}/Delete - [HttpPost("{recipeId}/Delete")] + [HttpDelete("{recipeId}/Delete")] [ValidateAntiForgeryToken] public async Task DeleteConfirmed(Guid recipeId) { - try + var deleted = await _recipeRepository.DeleteRecipeAsync(recipeId); + + if (deleted) { - await _recipeRepository.DeleteRecipeAsync(recipeId); TempData["SuccessMessage"] = "Rezept wurde erfolgreich gelöscht."; return RedirectToAction("Index", "Home"); } - catch (Exception ex) + else { - TempData["ErrorMessage"] = $"Ein Fehler ist aufgetreten: {ex.Message}"; + TempData["ErrorMessage"] = "Rezept nicht gefunden oder konnte nicht gelöscht werden."; return RedirectToAction("Details", new { recipeId }); } } From 0c3b7380c5e9dd9c1c9aa009bf9586d211664c07 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 20 May 2025 14:23:46 +0200 Subject: [PATCH 13/40] Refactoring some Repo methods to not throw exception --- .../Repositories/Recipe/IRecipeRepository.cs | 2 +- .../Repositories/Recipe/RecipeRepository.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index 2e4b375..5afb2f0 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -19,6 +19,6 @@ Task> SearchInRecipesAndIngredients(string searchterm); - Task DeleteRecipeAsync(Guid recipeId); + Task DeleteRecipeAsync(Guid recipeId); } } diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index 4492d1e..73c76a7 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -184,13 +184,13 @@ .ToListAsync(); } - public async Task DeleteRecipeAsync(Guid recipeId) + public async Task DeleteRecipeAsync(Guid recipeId) { var recipe = await GetRecipeByIdAsync(recipeId); if (recipe == null) { - throw new InvalidDataException($"Rezept mit ID {recipeId} nicht gefunden."); + return false; } if (recipe.RecipeIngredients != null && recipe.RecipeIngredients.Any()) @@ -224,6 +224,7 @@ _context.Recipes.Remove(recipe); await _context.SaveChangesAsync(); + return true; } } } From 06ef8927a7a6160067d66f4fba14ca26699e5942 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 20 May 2025 14:25:19 +0200 Subject: [PATCH 14/40] Use one way of loading for avoid inconsistency --- Francesco.Recipes.World/Views/Recipe/Details.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml index e30b936..777ebce 100644 --- a/Francesco.Recipes.World/Views/Recipe/Details.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Details.cshtml @@ -24,7 +24,7 @@

Cooking Time: @Model.CookingTime

- + @await Html.PartialAsync("_AdjustableIngredientsPartial", Model) @await Html.PartialAsync("_RecipeInstructionGridPartial", new Francesco.Recipes.World.Models.InstructionViewModel { From f89cf27be46b57ced2bd8fde7e1b80c55c1da224 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 20 May 2025 14:26:14 +0200 Subject: [PATCH 15/40] Add LIst instead of use an 2 dimensional array --- .../Views/Shared/_RecipeInstructionGridPartial.cshtml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml index 2a7b44b..ed4f682 100644 --- a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml @@ -6,12 +6,16 @@
@{ var groupedInstructions = Model.Instructions.OrderBy(i => i.Number).ToList(); - var rows = (int)Math.Ceiling(groupedInstructions.Count / 3.0); + var instructionRows = groupedInstructions + .Select((instruction, index) => new { Instruction = instruction, Index = index }) + .GroupBy(x => x.Index / 3) + .Select(g => g.Select(x => x.Instruction).ToList()) + .ToList(); - for (int row = 0; row < rows; row++) + foreach (var row in instructionRows) {
- @for (int col = 0; col < 3 && (row * 3 + col < groupedInstructions.Count); col++) + @for (var instruction in row) { var instruction = groupedInstructions[row * 3 + col];
From 924b896a9a168811183d73678a8948408f3e007a Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 20 May 2025 14:26:42 +0200 Subject: [PATCH 16/40] Remove empty line --- Francesco.Recipes.World/Views/Shared/_Layout.cshtml | 1 - 1 file changed, 1 deletion(-) diff --git a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml index 8eae480..55fbcc0 100644 --- a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml @@ -8,7 +8,6 @@ -
From f2bb057a9bb54b43ce944f8da8c4e75f5ea23be6 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 27 May 2025 13:47:13 +0200 Subject: [PATCH 17/40] Change from HttpPost to HttpDelete --- Francesco.Recipes.World/Controller/Recipe/RecipeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 9d31328..46177ee 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -497,7 +497,7 @@ return View(recipe); } - // POST: /Recipe/{recipeId}/Delete + // DELETE: /Recipe/{recipeId}/Delete [HttpDelete("{recipeId}/Delete")] [ValidateAntiForgeryToken] public async Task DeleteConfirmed(Guid recipeId) From 5c9f6c812069c9af55567171bebc764e39bd5be1 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 27 May 2025 13:47:51 +0200 Subject: [PATCH 18/40] add comment for a specification of this code --- ...4459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs | 3 --- Francesco.Recipes.World/Views/Recipe/Details.cshtml | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs index 6807402..e98a564 100644 --- a/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs +++ b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs @@ -1,11 +1,8 @@ // -using System; using Francesco.Recipes.World.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml index 777ebce..545b9f3 100644 --- a/Francesco.Recipes.World/Views/Recipe/Details.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Details.cshtml @@ -123,6 +123,8 @@ finalQuantity = Math.round(adjustedQuantity); } } + // Convert small unit "msp" (pinch) to teaspoons (TL) for a better estimate + // 4 pinches = about 1 TL → show that as a note, rounded to 1 decimal else if (specialUnits.smallUnits.includes(unitSymbol)) { if (adjustedQuantity > 3) { finalQuantity = Math.round(adjustedQuantity / 4 * 10) / 10; From 907e19e3983681d282cc67dc31e2eef1a6ec6417 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 27 May 2025 13:48:39 +0200 Subject: [PATCH 19/40] Make the code more readable and easier with some style --- .../_RecipeInstructionGridPartial.cshtml | 52 ++++++---------- Francesco.Recipes.World/wwwroot/css/site.css | 59 ++++++++++--------- 2 files changed, 48 insertions(+), 63 deletions(-) diff --git a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml index ed4f682..c814599 100644 --- a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml @@ -4,44 +4,28 @@

Instructions

- @{ - var groupedInstructions = Model.Instructions.OrderBy(i => i.Number).ToList(); - var instructionRows = groupedInstructions - .Select((instruction, index) => new { Instruction = instruction, Index = index }) - .GroupBy(x => x.Index / 3) - .Select(g => g.Select(x => x.Instruction).ToList()) - .ToList(); - - foreach (var row in instructionRows) - { -
- @for (var instruction in row) + @foreach (var instruction in Model.Instructions.OrderBy(i => i.Number)) + { +
+
+ @if (instruction.MediaFiles != null && instruction.MediaFiles.Any()) { - var instruction = groupedInstructions[row * 3 + col]; -
-
- @if (instruction.MediaFiles != null && instruction.MediaFiles.Any()) - { - var mediaFile = instruction.MediaFiles.First(); - @if (mediaFile.Data != null && mediaFile.Data.Length > 0) - { - Step @instruction.Number - } - } - - else - { -
- -
- } - @instruction.Number -
-

@instruction.Description

+ var mediaFile = instruction.MediaFiles.First(); + if (mediaFile.Data != null && mediaFile.Data.Length > 0) + { + Step @instruction.Number + } + } + else + { +
+
} + @instruction.Number
- } +

@instruction.Description

+
}
diff --git a/Francesco.Recipes.World/wwwroot/css/site.css b/Francesco.Recipes.World/wwwroot/css/site.css index b8d4e75..c895bb3 100644 --- a/Francesco.Recipes.World/wwwroot/css/site.css +++ b/Francesco.Recipes.World/wwwroot/css/site.css @@ -89,38 +89,36 @@ textarea.form-control { /* Recipe Instructions Grid Layout */ .instructions-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; margin-top: 2rem; margin-bottom: 2rem; } -.instruction-row { - display: flex; - justify-content: space-between; - margin-bottom: 2.5rem; - flex-wrap: wrap; -} - -.instruction-card { - width: calc(33.333% - 20px); - margin-bottom: 1.5rem; -} - @media (max-width: 992px) { - .instruction-card { - width: calc(50% - 15px); + .instructions-grid { + grid-template-columns: repeat(2, 1fr); } } @media (max-width: 576px) { - .instruction-card { - width: 100%; + .instructions-grid { + grid-template-columns: 1fr; } } +.instruction-card { + border: 1px solid #ccc; + padding: 1rem; + border-radius: 8px; + background-color: white; +} + .instruction-image { position: relative; width: 100%; - aspect-ratio: 1/1; + aspect-ratio: 1 / 1; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; @@ -129,36 +127,38 @@ textarea.form-control { align-items: center; justify-content: center; background-color: #f8f9fa; + text-align: center; } .instruction-image img { width: 100%; height: 100%; object-fit: cover; + border-radius: 4px; } .placeholder-image { - display: flex; - align-items: center; - justify-content: center; width: 100%; height: 100%; - color: #aaa; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + color: #999; + background-color: #f0f0f0; + border-radius: 4px; } - .placeholder-image i { - font-size: 2rem; - } - .step-number { position: absolute; - bottom: 5px; - right: 10px; - background-color: rgba(0, 0, 0, 0.5); + bottom: 8px; + right: 8px; + background: rgba(0, 0, 0, 0.6); color: white; - font-weight: bold; padding: 3px 8px; border-radius: 50%; + font-size: 0.875rem; + font-weight: bold; } .instruction-text { @@ -183,3 +183,4 @@ textarea.form-control { background-color: #007bff; } + From c503e008a7653473fb7b3e3267b8e1a1f05d91bd Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 27 May 2025 14:46:59 +0200 Subject: [PATCH 20/40] - remove blank line --- .../Views/Shared/_RecipeInstructionGridPartial.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml index c814599..c9b7df8 100644 --- a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml @@ -28,4 +28,4 @@
}
-
+
\ No newline at end of file From bb049e674a2cb7ea41eddf82dea6a5b91400f2f1 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 08:59:35 +0200 Subject: [PATCH 21/40] Make an Endpoint for sort favorit to newsest and oldest --- .../Controller/Favorite/FavoriteController.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs diff --git a/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs b/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs new file mode 100644 index 0000000..0198842 --- /dev/null +++ b/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs @@ -0,0 +1,34 @@ +namespace Francesco.Recipes.World.Controller.Favorite +{ + using Francesco.Recipes.World.Models; + using Francesco.Recipes.World.Repositories.Favorit; + using Microsoft.AspNetCore.Mvc; + + [Route("Favorite")] + public class FavoriteController : Controller + { + private readonly IFavoriteRepository _favoriteRepository; + + public FavoriteController(IFavoriteRepository favoriteRepository) + { + _favoriteRepository = favoriteRepository; + } + + public async Task Index(string sortOrder = "newest") + { + var favoriteRecipes = await _favoriteRepository.GetFavoriteRecipesAsync(); + + var sortedRecipes = sortOrder == "oldest" + ? favoriteRecipes.OrderBy(r => r.Favorit.CreatedAt) + : favoriteRecipes.OrderByDescending(r => r.Favorit.CreatedAt); + + var viewModel = new FavoriteViewModel + { + FavoriteRecipes = sortedRecipes, + SortOrder = sortOrder, + }; + + return View(viewModel); + } + } +} From c9f3f2b3436f5ed889d1d269c000be25f825a4b9 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:03:19 +0200 Subject: [PATCH 22/40] Make an viewmodel for the favorit sort --- .../Models/FavoriteViewModel.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Francesco.Recipes.World/Models/FavoriteViewModel.cs diff --git a/Francesco.Recipes.World/Models/FavoriteViewModel.cs b/Francesco.Recipes.World/Models/FavoriteViewModel.cs new file mode 100644 index 0000000..d6aa10b --- /dev/null +++ b/Francesco.Recipes.World/Models/FavoriteViewModel.cs @@ -0,0 +1,15 @@ +using Francesco.Recipes.World.Models.BackendModels.Recipe; + +namespace Francesco.Recipes.World.Models +{ + public class FavoriteViewModel + { + public IEnumerable FavoriteRecipes { get; set; } = new List(); + + public string SortOrder { get; set; } = "newest"; + + public bool HasFavorites => FavoriteRecipes.Any(); + + public string SortOrderDisplayText => SortOrder == "oldest" ? "Älteste Favorits" : "Neueste Favorits"; + } +} From 967542a0f1d30e7541a473bc83a7c44df7c79a90 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:06:14 +0200 Subject: [PATCH 23/40] Make a viewmodel for the main search --- .../Models/SearchViewModel.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Francesco.Recipes.World/Models/SearchViewModel.cs diff --git a/Francesco.Recipes.World/Models/SearchViewModel.cs b/Francesco.Recipes.World/Models/SearchViewModel.cs new file mode 100644 index 0000000..c148d5e --- /dev/null +++ b/Francesco.Recipes.World/Models/SearchViewModel.cs @@ -0,0 +1,21 @@ +namespace Francesco.Recipes.World.Models +{ + public class SearchViewModel + { + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public bool IsFavorite { get; set; } + + public byte[]? ImageData { get; set; } + + public string? MimeType { get; set; } + + public List Ingredients { get; set; } = new (); + + public TimeSpan TotalTime { get; set; } + } +} From fcf03d0f6219e57ed96e3216e73085a7fe35695d Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:07:27 +0200 Subject: [PATCH 24/40] Use AsSplitQuery for optimize loading related data --- .../Repositories/Category/CategoryRepository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs index 821f14f..501b240 100644 --- a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs +++ b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs @@ -47,6 +47,7 @@ return await _context.Categories .Include(c => c.Recipes) .ThenInclude(r => r.MediaFiles) + .AsSplitQuery() // Use AsSplitQuery to optimize loading related data .ToListAsync(); } } From 9ab1efde0e0c47914cb79c6781fe490d540e0cfe Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:08:34 +0200 Subject: [PATCH 25/40] Use AsSplitQuery to optimize loading related data --- .../Repositories/Category/CategoryRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs index 501b240..e3e6d11 100644 --- a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs +++ b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs @@ -47,7 +47,7 @@ return await _context.Categories .Include(c => c.Recipes) .ThenInclude(r => r.MediaFiles) - .AsSplitQuery() // Use AsSplitQuery to optimize loading related data + .AsSplitQuery() .ToListAsync(); } } From ba619888403580b5974cd2e92cab074e196f3807 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:18:38 +0200 Subject: [PATCH 26/40] Ensure that all favorites are retrieved, and when a recipe is added to favorites, include the creation date and time. --- .../Repositories/Favorit/FavoritRepository.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs index eb8cae2..9993adc 100644 --- a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs +++ b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs @@ -16,8 +16,10 @@ public async Task> GetFavoriteRecipesAsync() { return await _context.Recipes - .Where(r => r.IsFavorite) - .ToListAsync(); + .Where(r => r.IsFavorite) + .Include(r => r.Favorit) + .Include(r => r.MediaFiles) + .ToListAsync(); } public async Task IsFavoriteAsync(Guid recipeId) @@ -28,10 +30,27 @@ public async Task AddFavoriteAsync(Guid recipeId) { - var recipe = await _context.Recipes.FindAsync(recipeId); + var recipe = await _context.Recipes + .Include(r => r.Favorit) + .FirstOrDefaultAsync(r => r.Id == recipeId); + if (recipe != null && !recipe.IsFavorite) { recipe.IsFavorite = true; + + if (recipe.Favorit == null || recipe.Favorit.Id == Guid.Empty) + { + recipe.Favorit = new Models.BackendModels.Favorit.Favorit + { + Id = Guid.NewGuid(), + CreatedAt = DateTime.Now, + }; + } + else + { + recipe.Favorit.CreatedAt = DateTime.Now; + } + await _context.SaveChangesAsync(); } } From e1dd694fb70d71cfa27d8cf827777f74a2713178 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:20:49 +0200 Subject: [PATCH 27/40] Optimize Searchfilter method --- .../Repositories/Recipe/IRecipeRepository.cs | 4 +- .../Repositories/Recipe/RecipeRepository.cs | 71 +++++++++++++++---- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index 5afb2f0..ebbc8c7 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -1,5 +1,6 @@ namespace Francesco.Recipes.World.Repositories.Recipe { + using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Models.BackendModels.Category; using Francesco.Recipes.World.Models.BackendModels.Recipe; @@ -17,8 +18,7 @@ Task CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime); - Task> SearchInRecipesAndIngredients(string searchterm); - Task DeleteRecipeAsync(Guid recipeId); + Task> SearchInRecipesAndIngredients(string searchTerm); } } diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index 73c76a7..d097f7d 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -1,6 +1,7 @@ namespace Francesco.Recipes.World.Repositories.Recipe { using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Models.BackendModels.Category; using Francesco.Recipes.World.Models.BackendModels.Ingredient; using Francesco.Recipes.World.Models.BackendModels.Recipe; @@ -148,21 +149,67 @@ await _context.SaveChangesAsync(); } - public async Task> SearchInRecipesAndIngredients(string searchTerm) + public async Task> SearchInRecipesAndIngredients(string searchTerm) { - var queryable = _context.Recipes - .Include(r => r.RecipeIngredients) - .ThenInclude(ri => ri.Ingredient) - .AsQueryable(); - - if (!string.IsNullOrWhiteSpace(searchTerm)) + try { - 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(searchTerm)) + { + return await _context.Recipes + .OrderByDescending(r => r.CreatedAt) + .Take(20) + .Select(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("image/")) + .Select(m => m.Data) + .FirstOrDefault(), + MimeType = r.MediaFiles + .Where(m => m.MimeType != null && m.MimeType.StartsWith("image/")) + .Select(m => m.MimeType) + .FirstOrDefault(), + Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(), + TotalTime = r.PreparationTime.Add(r.CookingTime), + }) + .ToListAsync(); + } - return await queryable.ToListAsync(); + var normalizedSearchTerm = searchTerm.ToLower(); + + return await _context.Recipes + .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}%"))) + .OrderByDescending(r => r.CreatedAt) + .Take(100) + .Select(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("image/")) + .Select(m => m.Data) + .FirstOrDefault(), + MimeType = r.MediaFiles + .Where(m => m.MimeType != null && m.MimeType.StartsWith("image/")) + .Select(m => m.MimeType) + .FirstOrDefault(), + Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(), + TotalTime = r.PreparationTime.Add(r.CookingTime), + }) + .ToListAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Error in SearchInRecipesAndIngredientsOptimized: {ex.Message}"); + return new List(); + } } public async Task> GetRecipesByDifficultyAsync(Difficulty? difficulty) From 294f6010854bc1848c65ae5854e38e8fcf54eff0 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:21:50 +0200 Subject: [PATCH 28/40] Make some simple improvments --- .../Views/Home/Index.cshtml | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/Francesco.Recipes.World/Views/Home/Index.cshtml b/Francesco.Recipes.World/Views/Home/Index.cshtml index 0c9e60c..031bd3b 100644 --- a/Francesco.Recipes.World/Views/Home/Index.cshtml +++ b/Francesco.Recipes.World/Views/Home/Index.cshtml @@ -1,45 +1,44 @@ @model IEnumerable -@Html.AntiForgeryToken() + @Html.AntiForgeryToken() -
- Willkommen -

Willkommen in der Rezept-App

-
+
+ Willkommen +

Willkommen in der Rezept-App

+
-
- -
+
+ +
-
+
-@foreach (var category in Model) -{ -
-
-

@category.Category.Name

- Alle @category.Category.Name-Rezepte anzeigen -
+ @foreach (var category in Model) + { +
+
+

@category.Category.Name

+ Alle @category.Category.Name-Rezepte anzeigen +
-
- @foreach (var recipe in category.Recipes) - { - var mediaFile = recipe.MediaFiles?.FirstOrDefault(); - var imageData = mediaFile?.Data; - var mimeType = mediaFile?.MimeType; +
+ @foreach (var recipe in category.Recipes) + { + var mediaFile = recipe.MediaFiles?.FirstOrDefault(); + var imageData = mediaFile?.Data; + var mimeType = mediaFile?.MimeType; - + } - -} - - + } From 28e60b79fa74644c817a6cc7ea238725156a22de Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:25:26 +0200 Subject: [PATCH 29/40] Refactor favorite button: wrap in form for better HTMX and AntiForgeryToken support --- .../Views/Shared/_FavoriteButton.cshtml | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml b/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml index 5cd045a..7db2e11 100644 --- a/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml @@ -1,25 +1,26 @@ @model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe - +
+ @Html.AntiForgeryToken() + + +
From c20ed984c22aa4ae4854ea2638b1632c2fbb53ee Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:36:55 +0200 Subject: [PATCH 30/40] Replaced direct access to DB model properties with a dedicated ViewModel. Added per-recipe HTMX form including AntiForgeryToken and dynamic star icon based on IsFavorite status. --- .../Views/Home/_SearchResultsPartial.cshtml | 115 ++++++++++-------- 1 file changed, 66 insertions(+), 49 deletions(-) diff --git a/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml b/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml index 4edbb64..c03a7fb 100644 --- a/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml +++ b/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml @@ -1,58 +1,75 @@ -@model IEnumerable +@model IEnumerable @{ if (!Model.Any()) -{ -

Keine Ergebnisse gefunden.

-} -else -{ -
- @foreach (var recipe in Model) - { -
-
-
- @{ - var mediaFile = recipe.MediaFiles.FirstOrDefault(); - - if (mediaFile?.Data != null) - { - @recipe.Name - } - else - { - Platzhalter - } - } -
- -
-
-
@recipe.Name
-

- - @recipe.PreparationTime.Hours h @recipe.PreparationTime.Minutes min -

+ { +

Keine Ergebnisse gefunden.

+ } + else + { +
+ @foreach (var recipe in Model) + { +
+
+
+ @if (recipe.ImageData != null && recipe.MimeType != null) + { + @recipe.Name + } + else + { + Platzhalter + }
-
- +
+
+
@recipe.Name
+

+ + @recipe.TotalTime.Hours h @recipe.TotalTime.Minutes min +

+
+ +
+
+
+ @Html.AntiForgeryToken() + + +
+
+ + Details +
-
- } -
+ } +
+ } } -} - From 075aefc97dd98af7ffdc1f92546991bfa5807273 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:37:19 +0200 Subject: [PATCH 31/40] Make Favorite Site --- .../Views/Favorite/Index.cshtml | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 Francesco.Recipes.World/Views/Favorite/Index.cshtml diff --git a/Francesco.Recipes.World/Views/Favorite/Index.cshtml b/Francesco.Recipes.World/Views/Favorite/Index.cshtml new file mode 100644 index 0000000..b1b84af --- /dev/null +++ b/Francesco.Recipes.World/Views/Favorite/Index.cshtml @@ -0,0 +1,77 @@ +@model Francesco.Recipes.World.Models.FavoriteViewModel +@{ + ViewData["Title"] = "Favoriten"; +} + +

Favoriten

+ +
+ +
+ +@if (!Model.HasFavorites) +{ +
+ Keine Favoriten vorhanden. Füge Rezepte zu deinen Favoriten hinzu, indem du auf den Stern klickst. +
+} +else +{ +
+ @foreach (var recipe in Model.FavoriteRecipes) + { +
+
+ @{ + var mediaFile = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType != null && m.MimeType.StartsWith("image/")); + var imageData = mediaFile?.Data; + var mimeType = mediaFile?.MimeType; + } + +
+ @if (imageData != null && mimeType != null) + { + @recipe.Name + } + else + { +
Kein Bild
+ } +
+ +
+
@recipe.Name
+
+ + @await Html.PartialAsync("_FavoriteButton", recipe) + + + @(recipe.PreparationTime.TotalMinutes + recipe.CookingTime.TotalMinutes)min + +
+
+ +
+
+ } +
+} From e468a4778290efe1d1aec826a540816e89cb9a74 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Mon, 2 Jun 2025 09:46:12 +0200 Subject: [PATCH 32/40] Replaced DbContext registration to use SplitQuery mode --- Francesco.Recipes.World/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index 1caf8cf..60f7325 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -20,7 +20,8 @@ var connectionString = builder.Configuration.GetConnectionString("FrancescosReci ?? throw new InvalidOperationException("Connection string 'FrancescosRecipesWorldDbContextConnection' not found."); services.AddDbContext(options => - options.UseSqlServer(connectionString)); + options.UseSqlServer(connectionString, sqlOptions => + sqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery))); // Add services to the container. builder.Services.AddControllersWithViews(); From 40690304fa0c998a738ef4238b0479d916bcbae3 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 3 Jun 2025 09:17:21 +0200 Subject: [PATCH 33/40] Make const folder with all necessary constants --- Francesco.Recipes.World/Constants/ContentType.cs | 7 +++++++ Francesco.Recipes.World/Constants/SortOrders.cs | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 Francesco.Recipes.World/Constants/ContentType.cs create mode 100644 Francesco.Recipes.World/Constants/SortOrders.cs diff --git a/Francesco.Recipes.World/Constants/ContentType.cs b/Francesco.Recipes.World/Constants/ContentType.cs new file mode 100644 index 0000000..8f7d090 --- /dev/null +++ b/Francesco.Recipes.World/Constants/ContentType.cs @@ -0,0 +1,7 @@ +namespace Francesco.Recipes.World.Constants +{ + public class ContentType + { + public const string Image = "image/"; + } +} diff --git a/Francesco.Recipes.World/Constants/SortOrders.cs b/Francesco.Recipes.World/Constants/SortOrders.cs new file mode 100644 index 0000000..479261c --- /dev/null +++ b/Francesco.Recipes.World/Constants/SortOrders.cs @@ -0,0 +1,8 @@ +namespace Francesco.Recipes.World.Constants +{ + public class SortOrders + { + public const string Newest = "newest"; + public const string Oldest = "oldest"; + } +} From febcdc1cedd5093d358736b5e47df19f27e8ecd3 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 3 Jun 2025 09:17:48 +0200 Subject: [PATCH 34/40] Change all magic strings with the consts --- .../Controller/Favorite/FavoriteController.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs b/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs index 0198842..9cd9c7a 100644 --- a/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs +++ b/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs @@ -1,5 +1,6 @@ namespace Francesco.Recipes.World.Controller.Favorite { + using Francesco.Recipes.World.Constants; using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Repositories.Favorit; using Microsoft.AspNetCore.Mvc; @@ -14,13 +15,13 @@ _favoriteRepository = favoriteRepository; } - public async Task Index(string sortOrder = "newest") + public async Task Index(string sortOrder = SortOrders.Newest) { var favoriteRecipes = await _favoriteRepository.GetFavoriteRecipesAsync(); - var sortedRecipes = sortOrder == "oldest" - ? favoriteRecipes.OrderBy(r => r.Favorit.CreatedAt) - : favoriteRecipes.OrderByDescending(r => r.Favorit.CreatedAt); + var sortedRecipes = sortOrder == SortOrders.Oldest + ? favoriteRecipes.OrderBy(r => r.Favorite.CreatedAt) + : favoriteRecipes.OrderByDescending(r => r.Favorite.CreatedAt); var viewModel = new FavoriteViewModel { From 17726a4b6302250a76845b255cc9ffcf07d6d34b Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 3 Jun 2025 09:18:26 +0200 Subject: [PATCH 35/40] Make new migration for renaming Favorit columns to Favorite --- ...602133408_RenameFavoriteColumn.Designer.cs | 571 ++++++++++++++++++ .../20250602133408_RenameFavoriteColumn.cs | 72 +++ ...escosRecipesWorldDbContextModelSnapshot.cs | 10 +- 3 files changed, 648 insertions(+), 5 deletions(-) create mode 100644 Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs create mode 100644 Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs diff --git a/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs new file mode 100644 index 0000000..b7ba81b --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs @@ -0,0 +1,571 @@ +// +using System; +using Francesco.Recipes.World.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + [DbContext(typeof(FrancescosRecipesWorldDbContext))] + [Migration("20250602133408_RenameFavoriteColumn")] + partial class RenameFavoriteColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"), + Name = "Vorspeisen & Snacks" + }, + new + { + Id = new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), + Name = "Erste Gänge" + }, + new + { + Id = new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), + Name = "Hauptgerichte" + }, + new + { + Id = new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"), + Name = "Desserts & Süßspeisen" + }, + new + { + Id = new Guid("28e39168-701a-4084-81da-d96c987c462f"), + Name = "Beilagen & Salate" + }, + new + { + Id = new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"), + Name = "Kuchen" + }, + new + { + Id = new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), + Name = "Hefegebäck & Brot" + }, + new + { + Id = new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"), + Name = "Soßen & Saucen" + }, + new + { + Id = new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), + Name = "Marmeladen & Eingemachtes" + }, + new + { + Id = new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), + Name = "Getränke" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Favorits"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsChecked") + .HasColumnType("bit"); + + b.Property("RecipeIngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeIngredientId"); + + b.HasIndex("RecipeShoppingListId"); + + b.ToTable("RecipeIngredientsShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.ToTable("Instructions"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("InstructionId") + .HasColumnType("uniqueidentifier"); + + b.Property("MimeType") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("InstructionId"); + + b.HasIndex("RecipeId"); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CookingTime") + .HasColumnType("time"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Difficulty") + .HasColumnType("int"); + + b.Property("FavoriteId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsFavorite") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PreparationTime") + .HasColumnType("time"); + + b.Property("Servings") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("FavoriteId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UnitId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("UnitId"); + + b.ToTable("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.HasIndex("ShoppingListId"); + + b.ToTable("RecipeShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("ShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Units"); + + b.HasData( + new + { + Id = new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), + Name = "liter", + Symbol = "l" + }, + new + { + Id = new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), + Name = "gramm", + Symbol = "g" + }, + new + { + Id = new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), + Name = "kilogramm", + Symbol = "kg" + }, + new + { + Id = new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), + Name = "stücke", + Symbol = "stk" + }, + new + { + Id = new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), + Name = "blatt", + Symbol = "blatt" + }, + new + { + Id = new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), + Name = "messerspitze", + Symbol = "msp" + }, + new + { + Id = new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), + Name = "stange", + Symbol = "stange" + }, + new + { + Id = new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"), + Name = "bund", + Symbol = "bund" + }, + new + { + Id = new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), + Name = "zehe", + Symbol = "zehe" + }, + new + { + Id = new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), + Name = "teelöffel", + Symbol = "TL" + }, + new + { + Id = new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), + Name = "esslöffel", + Symbol = "EL" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null) + .WithMany("IngredientShoppingLists") + .HasForeignKey("IngredientId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient") + .WithMany() + .HasForeignKey("RecipeIngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") + .WithMany("SelectedIngredients") + .HasForeignKey("RecipeShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RecipeIngredient"); + + b.Navigation("RecipeShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("Instructions") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction") + .WithMany("MediaFiles") + .HasForeignKey("InstructionId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("MediaFiles") + .HasForeignKey("RecipeId"); + + b.Navigation("Instruction"); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category") + .WithMany("Recipes") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorite") + .WithMany("Recipe") + .HasForeignKey("FavoriteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Favorite"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") + .WithMany("RecipeIngredients") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("RecipeIngredients") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit") + .WithMany("RecipeIngredient") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("Recipe"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") + .WithMany("RecipeShoppingList") + .HasForeignKey("ShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + + b.Navigation("ShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Navigation("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Navigation("IngredientShoppingLists"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Navigation("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Navigation("Instructions"); + + b.Navigation("MediaFiles"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Navigation("SelectedIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Navigation("RecipeShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Navigation("RecipeIngredient"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs new file mode 100644 index 0000000..79582f6 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + /// + public partial class RenameFavoriteColumn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_Recipes_Favorits_FavoritId", + table: "Recipes"); + + migrationBuilder.RenameColumn( + name: "FavoritId", + table: "Recipes", + newName: "FavoriteId"); + + migrationBuilder.RenameIndex( + name: "IX_Recipes_FavoritId", + table: "Recipes", + newName: "IX_Recipes_FavoriteId"); + + migrationBuilder.AddForeignKey( + name: "FK_Recipes_Favorits_FavoriteId", + table: "Recipes", + column: "FavoriteId", + principalTable: "Favorits", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_Recipes_Favorits_FavoriteId", + table: "Recipes"); + + migrationBuilder.RenameColumn( + name: "FavoriteId", + table: "Recipes", + newName: "FavoritId"); + + migrationBuilder.RenameIndex( + name: "IX_Recipes_FavoriteId", + table: "Recipes", + newName: "IX_Recipes_FavoritId"); + + migrationBuilder.AddForeignKey( + name: "FK_Recipes_Favorits_FavoritId", + table: "Recipes", + column: "FavoritId", + principalTable: "Favorits", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs index 5def152..67067f0 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -221,7 +221,7 @@ namespace Francesco.Recipes.World.Migrations b.Property("Difficulty") .HasColumnType("int"); - b.Property("FavoritId") + b.Property("FavoriteId") .HasColumnType("uniqueidentifier"); b.Property("IsFavorite") @@ -244,7 +244,7 @@ namespace Francesco.Recipes.World.Migrations b.HasIndex("CategoryId"); - b.HasIndex("FavoritId"); + b.HasIndex("FavoriteId"); b.ToTable("Recipes"); }); @@ -460,15 +460,15 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorite") .WithMany("Recipe") - .HasForeignKey("FavoritId") + .HasForeignKey("FavoriteId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Category"); - b.Navigation("Favorit"); + b.Navigation("Favorite"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => From 0f274613debe1fb0258a135b5a87e7a49ae7026f Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 3 Jun 2025 09:19:04 +0200 Subject: [PATCH 36/40] Change magic strings with the consts --- Francesco.Recipes.World/Models/FavoriteViewModel.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Francesco.Recipes.World/Models/FavoriteViewModel.cs b/Francesco.Recipes.World/Models/FavoriteViewModel.cs index d6aa10b..b3412c6 100644 --- a/Francesco.Recipes.World/Models/FavoriteViewModel.cs +++ b/Francesco.Recipes.World/Models/FavoriteViewModel.cs @@ -1,4 +1,5 @@ -using Francesco.Recipes.World.Models.BackendModels.Recipe; +using Francesco.Recipes.World.Constants; +using Francesco.Recipes.World.Models.BackendModels.Recipe; namespace Francesco.Recipes.World.Models { @@ -6,10 +7,10 @@ namespace Francesco.Recipes.World.Models { public IEnumerable FavoriteRecipes { get; set; } = new List(); - public string SortOrder { get; set; } = "newest"; + public string SortOrder { get; set; } = SortOrders.Newest; public bool HasFavorites => FavoriteRecipes.Any(); - public string SortOrderDisplayText => SortOrder == "oldest" ? "Älteste Favorits" : "Neueste Favorits"; + public string SortOrderDisplayText => SortOrder == SortOrders.Oldest ? "Älteste Favorits" : "Neueste Favorits"; } } From 3ae229b4f342faee6b60c3ede238798092334767 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 3 Jun 2025 09:19:44 +0200 Subject: [PATCH 37/40] ChangeFavorit to Favorite and change some method logic --- .../Repositories/Favorit/FavoritRepository.cs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs index 9993adc..5ca4fa3 100644 --- a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs +++ b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs @@ -17,7 +17,7 @@ { return await _context.Recipes .Where(r => r.IsFavorite) - .Include(r => r.Favorit) + .Include(r => r.Favorite) .Include(r => r.MediaFiles) .ToListAsync(); } @@ -31,28 +31,31 @@ public async Task AddFavoriteAsync(Guid recipeId) { var recipe = await _context.Recipes - .Include(r => r.Favorit) + .Include(r => r.Favorite) .FirstOrDefaultAsync(r => r.Id == recipeId); - if (recipe != null && !recipe.IsFavorite) + if (recipe == null) { - recipe.IsFavorite = true; - - if (recipe.Favorit == null || recipe.Favorit.Id == Guid.Empty) - { - recipe.Favorit = new Models.BackendModels.Favorit.Favorit - { - Id = Guid.NewGuid(), - CreatedAt = DateTime.Now, - }; - } - else - { - recipe.Favorit.CreatedAt = DateTime.Now; - } - - await _context.SaveChangesAsync(); + throw new InvalidOperationException("Recipe not found."); } + + if (recipe.IsFavorite) + { + throw new InvalidOperationException("Recipe is already a favorite."); + } + + recipe.IsFavorite = true; + + if (recipe.Favorite == null || recipe.Favorite.Id == Guid.Empty) + { + recipe.Favorite = new Models.BackendModels.Favorit.Favorit + { + Id = Guid.NewGuid(), + CreatedAt = DateTime.Now, + }; + } + + await _context.SaveChangesAsync(); } public async Task RemoveFavoriteAsync(Guid recipeId) From a0e9e8311813f66b7fb080c53587afd9a1b4f457 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 3 Jun 2025 09:20:14 +0200 Subject: [PATCH 38/40] Change magic strings with consts --- .../Repositories/MediaFile/MediaFileRepository.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs index d4a8d9a..d929250 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -1,5 +1,6 @@ namespace Francesco.Recipes.World.Repositories.MediaFile { + using Francesco.Recipes.World.Constants; using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models.BackendModels.MediaFile; using Francesco.Recipes.World.Models.BackendModels.Recipe; @@ -83,7 +84,7 @@ { var recipe = await _recipeRepository.GetRecipeAsync(recipeId); - var isImage = mediaFile.ContentType.StartsWith("image/"); + var isImage = mediaFile.ContentType.StartsWith(ContentType.Image); var isVideo = mediaFile.ContentType.StartsWith("video/"); if (!isImage && !isVideo) @@ -93,7 +94,7 @@ if (isImage) { - await RemoveExistingMediaAsync(recipe, "image/"); + await RemoveExistingMediaAsync(recipe, ContentType.Image); } else { From 9d90a54830ef67a5aff285472329e2d3f8b5e85e Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 3 Jun 2025 09:20:41 +0200 Subject: [PATCH 39/40] Refactor some code in one method --- .../Repositories/Recipe/RecipeRepository.cs | 74 +++++++++---------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index d097f7d..3f8c683 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -1,5 +1,7 @@ namespace Francesco.Recipes.World.Repositories.Recipe { + using System.Linq.Expressions; + using Francesco.Recipes.World.Constants; using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Models.BackendModels.Category; @@ -158,52 +160,17 @@ return await _context.Recipes .OrderByDescending(r => r.CreatedAt) .Take(20) - .Select(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("image/")) - .Select(m => m.Data) - .FirstOrDefault(), - MimeType = r.MediaFiles - .Where(m => m.MimeType != null && m.MimeType.StartsWith("image/")) - .Select(m => m.MimeType) - .FirstOrDefault(), - Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(), - TotalTime = r.PreparationTime.Add(r.CookingTime), - }) + .Select(SearchViewModelSelector()) .ToListAsync(); } var normalizedSearchTerm = searchTerm.ToLower(); - return await _context.Recipes - .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}%"))) + return await ApplyRecipeSearchFilter(_context.Recipes, normalizedSearchTerm) .OrderByDescending(r => r.CreatedAt) .Take(100) - .Select(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("image/")) - .Select(m => m.Data) - .FirstOrDefault(), - MimeType = r.MediaFiles - .Where(m => m.MimeType != null && m.MimeType.StartsWith("image/")) - .Select(m => m.MimeType) - .FirstOrDefault(), - Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(), - TotalTime = r.PreparationTime.Add(r.CookingTime), - }) - .ToListAsync(); + .Select(SearchViewModelSelector()) + .ToListAsync(); } catch (Exception ex) { @@ -231,6 +198,35 @@ .ToListAsync(); } + private static Expression> 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 ApplyRecipeSearchFilter(IQueryable 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 DeleteRecipeAsync(Guid recipeId) { var recipe = await GetRecipeByIdAsync(recipeId); From 78dbfa20f1b0c95bab552057f09dfa31db308607 Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Tue, 3 Jun 2025 09:21:07 +0200 Subject: [PATCH 40/40] Change Favorit to Favorite --- Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs index 977b9ad..9e688a8 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs @@ -34,7 +34,7 @@ public class Recipe : ITimeStampedEntity public virtual ICollection MediaFiles { get; set; } = new List(); - public virtual Favorit Favorit { get; set; } = new (); + public virtual Favorit Favorite { get; set; } = new (); public virtual Category Category { get; set; } = new (); }