From 9e7ff56099015c5d16b2c3d31115f1af58359c2c Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 30 Apr 2025 18:51:36 +0200 Subject: [PATCH 01/30] Make New Migration to make Instruction nullable for avoid data inconsistenc --- ...InstructionOptionalInMediaFile.Designer.cs | 571 ++++++++++++++++++ ...4852_MakeInstructionOptionalInMediaFile.cs | 69 +++ ...escosRecipesWorldDbContextModelSnapshot.cs | 10 +- 3 files changed, 644 insertions(+), 6 deletions(-) create mode 100644 Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.Designer.cs create mode 100644 Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.cs diff --git a/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.Designer.cs b/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.Designer.cs new file mode 100644 index 0000000..6344cb6 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.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("20250430094852_MakeInstructionOptionalInMediaFile")] + partial class MakeInstructionOptionalInMediaFile + { + /// + 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("FavoritId") + .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("FavoritId"); + + 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", "Favorit") + .WithMany("Recipe") + .HasForeignKey("FavoritId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Favorit"); + }); + + 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/20250430094852_MakeInstructionOptionalInMediaFile.cs b/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.cs new file mode 100644 index 0000000..b6fb4f7 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + /// + public partial class MakeInstructionOptionalInMediaFile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + table: "MediaFiles"); + + migrationBuilder.AlterColumn( + name: "InstructionId", + table: "MediaFiles", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + table: "MediaFiles", + column: "InstructionId", + principalTable: "Instructions", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + table: "MediaFiles"); + + migrationBuilder.AlterColumn( + name: "InstructionId", + table: "MediaFiles", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + table: "MediaFiles", + column: "InstructionId", + principalTable: "Instructions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs index c27540a..5def152 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -182,7 +182,7 @@ namespace Francesco.Recipes.World.Migrations b.Property("FileName") .HasColumnType("nvarchar(max)"); - b.Property("InstructionId") + b.Property("InstructionId") .HasColumnType("uniqueidentifier"); b.Property("MimeType") @@ -441,9 +441,7 @@ namespace Francesco.Recipes.World.Migrations { b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction") .WithMany("MediaFiles") - .HasForeignKey("InstructionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("InstructionId"); b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") .WithMany("MediaFiles") @@ -509,7 +507,7 @@ namespace Francesco.Recipes.World.Migrations .IsRequired(); b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") - .WithMany("RecipeIngredientShoppingLists") + .WithMany("RecipeShoppingList") .HasForeignKey("ShoppingListId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -557,7 +555,7 @@ namespace Francesco.Recipes.World.Migrations modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => { - b.Navigation("RecipeIngredientShoppingLists"); + b.Navigation("RecipeShoppingList"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => From 8b3489cf630333180b64b0ef98647a1ac2cb5383 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 30 Apr 2025 18:53:20 +0200 Subject: [PATCH 02/30] Add viewModels for creatingRecipes, Instruction and ingredient of Recipe --- .../Controller/Recipe/RecipeController.cs | 173 +++++++++++++++--- 1 file changed, 152 insertions(+), 21 deletions(-) diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 72bf903..d7abfaa 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -1,5 +1,7 @@ namespace Francesco.Recipes.World.Controller.Recipe { + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Category; using Francesco.Recipes.World.Repositories.Favorit; @@ -12,6 +14,7 @@ using Francesco.Recipes.World.Views.Recipe; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; + using Microsoft.EntityFrameworkCore; public class RecipeController : Controller { @@ -22,6 +25,7 @@ private readonly IMediaFileRepository _mediaFileRepository; private readonly IInstructionRepository _instructionRepository; private readonly IFavoriteRepository _favoriteRepository; + private readonly FrancescosRecipesWorldDbContext _context; public IReadOnlyCollection Recipes { get; set; } @@ -32,7 +36,8 @@ IIngredientRepository ingredientRepository, IMediaFileRepository mediaFileRepository, IInstructionRepository instructionRepository, - IFavoriteRepository favoriteRepository) + IFavoriteRepository favoriteRepository, + FrancescosRecipesWorldDbContext context) { _recipeRepository = recipeRepository; _unitRepository = unitRepository; @@ -42,6 +47,7 @@ _mediaFileRepository = mediaFileRepository; _instructionRepository = instructionRepository; _favoriteRepository = favoriteRepository; + _context = context; } // GET: /Recipe/{recipeId}/AddOrCreateIngredient @@ -49,9 +55,17 @@ public async Task AddOrCreateIngredient(Guid recipeId) { var units = await _unitRepository.GetAllUnitsAsync(); - ViewBag.Units = new SelectList(units, "Id", "Name"); - ViewBag.RecipeId = recipeId; - return View(); + var recipeIngredients = await _ingredientRepository.GetIngredientsByRecipeIdAsync(recipeId); + var ingredients = recipeIngredients.Select(ri => ri.Ingredient).ToList(); + + var viewModel = new IngredientViewModel + { + RecipeId = recipeId, + Ingredients = ingredients, + Units = units.ToList(), + }; + + return View(viewModel); } // GET: /Recipe/Details/{recipeId} @@ -113,23 +127,47 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } + var units = await _unitRepository.GetAllUnitsAsync(); + ViewBag.Units = new SelectList(units, "Id", "Name"); + + var viewModel = new CreateRecipeViewModel + { + CategoryId = categoryId, + IngredientViewModel = new IngredientViewModel + { + RecipeId = Guid.Empty, + Ingredients = new List(), + Units = units.ToList(), + }, + InstructionViewModel = new InstructionViewModel + { + RecipeId = Guid.Empty, + Instructions = new List(), + }, + }; + ViewBag.CategoryName = category.Name; - return View(); + return View(viewModel); } // POST: /Recipe/Create/{categoryId} [HttpPost("Create/{categoryId}")] - [AutoValidateAntiforgeryToken] - public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime, IFormFile? photo) + [ValidateAntiForgeryToken] + public async Task Create(Guid categoryId, CreateRecipeViewModel model, IFormFile? photo, IFormFile? video) { - if (string.IsNullOrWhiteSpace(name)) + if (model == null) { - ModelState.AddModelError(nameof(name), "Name darf nicht leer sein."); + throw new ArgumentNullException(nameof(model), "CreateRecipeViewModel cannot be null."); } - if (servings <= 0) + if (string.IsNullOrWhiteSpace(model.Name)) { - ModelState.AddModelError(nameof(servings), "Anzahl der Portionen muss größer als 0 sein."); + ModelState.AddModelError(nameof(model.Name), "Name darf nicht leer sein."); + } + + if (model.Servings <= 0) + { + ModelState.AddModelError(nameof(model.Servings), "Anzahl der Portionen muss größer als 0 sein."); } if (!ModelState.IsValid) @@ -140,8 +178,10 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } + var units = await _unitRepository.GetAllUnitsAsync(); + ViewBag.Units = new SelectList(units, "Id", "Name"); ViewBag.CategoryName = category.Name; - return View(); + return View(model); } var categoryEntity = await _categoryRepository.GetCategoryByIdAsync(categoryId); @@ -150,14 +190,59 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } - await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); - var recipe = await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); + model.PreparationTime = new TimeSpan(model.PrepHours, model.PrepMinutes, 0); + model.CookingTime = new TimeSpan(model.CookHours, model.CookMinutes, 0); + + var recipe = await _recipeRepository.CreateRecipeForCategoryAsync( + categoryEntity, + model.Name, + model.Description ?? string.Empty, + model.Difficulty, + model.Servings, + model.PreparationTime, + model.CookingTime); + if (photo != null) { await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, photo); } - return RedirectToAction("Details", "Category", new { id = categoryId }); + if (video != null) + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, video); + } + + if (model.IngredientViewModel?.Ingredients != null) + { + foreach (var ingredient in model.IngredientViewModel.Ingredients) + { + var ri = ingredient.RecipeIngredients?.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(ingredient.Name) && ri?.Quantity > 0 && ri?.Unit?.Id != null) + { + await _recipeRepository.AddOrCreateIngredientToRecipeAsync( + recipe.Id, + ingredient.Name, + ri.Quantity, + ri.Unit.Id); + } + } + } + + if (model.InstructionViewModel?.Instructions != null) + { + foreach (var instruction in model.InstructionViewModel.Instructions) + { + if (!string.IsNullOrWhiteSpace(instruction.Description)) + { + await _instructionRepository.CreateInstructionWithImageToRecipeAsync( + recipe.Id, + instruction.Description, + null); + } + } + } + + return RedirectToAction("Details", new { recipeId = recipe.Id }); } // GET: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} @@ -252,26 +337,55 @@ return NotFound("Recipe not found."); } - ViewBag.RecipeId = recipeId; - return View(recipe); + var instructions = await _context.Instructions + .Include(i => i.MediaFiles) + .Where(i => i.Recipe.Id == recipeId) + .OrderBy(i => i.Number) + .ToListAsync(); + + var viewModel = new InstructionViewModel + { + RecipeId = recipeId, + Instructions = instructions, + }; + + return View(viewModel); } // POST: /Recipe/{recipeId}/AddInstruction [HttpPost("{recipeId}/AddInstruction")] [ValidateAntiForgeryToken] - public async Task AddInstruction(Guid recipeId, string description) + public async Task AddInstruction(Guid recipeId, string description, IFormFile? image) { try { - await _instructionRepository.CreateInstructionToRecipeAsync(recipeId, description); + await _instructionRepository.CreateInstructionWithImageToRecipeAsync(recipeId, description, image); + + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + var instructions = recipe?.Instructions?.ToList() ?? new List(); + + var viewModel = new InstructionViewModel + { + RecipeId = recipeId, + Instructions = instructions, + }; + return RedirectToAction("AddInstruction", new { recipeId }); } catch (Exception ex) { ModelState.AddModelError(string.Empty, ex.Message); + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); - ViewBag.RecipeId = recipeId; - return View(recipe); + var instructions = recipe?.Instructions?.ToList() ?? new List(); + + var viewModel = new InstructionViewModel + { + RecipeId = recipeId, + Instructions = instructions, + }; + + return View(viewModel); } } @@ -312,5 +426,22 @@ return PartialView("_FavoriteButton", recipe); } + + // GET: /Recipe/{recipeId}/GetIngredients + [HttpGet("{recipeId}/GetIngredients")] + public async Task GetIngredients(Guid recipeId) + { + var recipeIngredients = (await _ingredientRepository.GetIngredientsByRecipeIdAsync(recipeId)).Select(ri => ri.Ingredient).ToList(); + var units = (await _unitRepository.GetAllUnitsAsync()).ToList(); + + var viewModel = new IngredientViewModel + { + RecipeId = recipeId, + Ingredients = recipeIngredients, + Units = units, + }; + + return PartialView("_IngredientsPartial", viewModel); + } } } From ca15b696770b202d2f299e47fb87007421b81057 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 30 Apr 2025 18:54:38 +0200 Subject: [PATCH 03/30] create a js file with all scripts --- Francesco.Recipes.World/wwwroot/js/site.js | 193 ++++++++++++++++++++- 1 file changed, 190 insertions(+), 3 deletions(-) diff --git a/Francesco.Recipes.World/wwwroot/js/site.js b/Francesco.Recipes.World/wwwroot/js/site.js index 0937657..c69a044 100644 --- a/Francesco.Recipes.World/wwwroot/js/site.js +++ b/Francesco.Recipes.World/wwwroot/js/site.js @@ -1,4 +1,191 @@ -// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification -// for details on configuring this project to bundle and minify static web assets. +htmx.on('htmx:afterSwap', (event) => { + if (event.target.id === 'instructions-container') { + console.log('Instructions reloaded.'); + } + if (event.target.id === 'ingredients-container') { + console.log('Ingredients reloaded.'); + } +}); + +let recipeId; +function setRecipeId(id) { + recipeId = id; +} + + +async function moveInstructionUp(instructionId, recipeIdParam) { + const idToUse = recipeIdParam || recipeId; + + if (!idToUse) { + alert('Recipe ID is not set. Please select a recipe first.'); + return; + } + + try { + const response = await fetch(`/Recipe/${idToUse}/Instruction/${instructionId}/move-up`, { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }); + + if (response.ok) { + htmx.ajax('GET', `/Recipe/${idToUse}/Instructions`, '#instructions-container'); + } else { + const error = await response.json(); + alert(error.Error || 'Failed to move instruction up.'); + } + } catch (error) { + console.error('Error moving instruction up:', error); + } +} + + +async function moveInstructionDown(instructionId, recipeIdParam) { + const idToUse = recipeIdParam || recipeId; + + if (!idToUse) { + alert('Recipe ID is not set. Please select a recipe first.'); + return; + } + + try { + const response = await fetch(`/Recipe/${idToUse}/Instruction/${instructionId}/move-down`, { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }); + + if (response.ok) { + htmx.ajax('GET', `/Recipe/${idToUse}/Instructions`, '#instructions-container'); + } else { + const error = await response.json(); + alert(error.Error || 'Failed to move instruction down.'); + } + } catch (error) { + console.error('Error moving instruction down:', error); + } +} + + +async function removeInstruction(instructionId, recipeIdParam) { + const idToUse = recipeIdParam || recipeId; + + if (!idToUse) { + alert('Recipe ID is not set. Please select a recipe first.'); + return; + } + + if (!confirm('Möchtest du diesen Schritt wirklich löschen?')) return; + + try { + const response = await fetch(`/${idToUse}/RemoveInstruction/${instructionId}`, { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }); + + if (response.ok) { + const element = document.getElementById(`instruction-${instructionId}`); + if (element) { + element.remove(); + } + } else { + const error = await response.json(); + alert(error.Error || 'Fehler beim Löschen der Anweisung.'); + } + } catch (error) { + console.error('Fehler beim Löschen:', error); + } +} + +function addInstruction() { + const container = document.getElementById('instructions-container'); + const index = document.querySelectorAll('.instruction-item').length; // Index for model binding + + const newInstructionHtml = ` +
+
+ + + +
+
+ + +
+
+ `; + container.insertAdjacentHTML('beforeend', newInstructionHtml); +} + + +async function removeIngredient(ingredientId) { + if (!confirm('Möchtest du diese Zutat wirklich löschen?')) return; + + try { + const response = await fetch(`/${recipeId}/RemoveIngredient/${ingredientId}`, { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }); + + if (response.ok) { + const element = document.getElementById(`ingredient-${ingredientId}`); + if (element) { + element.remove(); + } + } else { + const error = await response.json(); + alert(error.Error || 'Fehler beim Löschen der Zutat.'); + } + } catch (error) { + console.error('Fehler beim Löschen:', error); + } +} + + +async function addIngredient() { + const container = document.getElementById('ingredients-container'); + const index = document.querySelectorAll('.ingredient-item').length; // Index for model binding + + const newIngredientHtml = ` +
+
+ + + + +
+
+ `; + container.insertAdjacentHTML('beforeend', newIngredientHtml); + + const addedElement = document.getElementById(`ingredient-new-${index}`); + const unitSelect = addedElement.querySelector('.unit-select'); + + try { + const response = await fetch('/Unit/GetAllUnits'); + if (response.ok) { + const units = await response.json(); + console.log('Fetched units:', units); + unitSelect.innerHTML = ''; + units.forEach(unit => { + const option = new Option(unit.name, unit.id); + unitSelect.add(option); + }); + } else { + console.error('Failed to fetch units'); + unitSelect.innerHTML = ''; + } + } catch (error) { + console.error('Error fetching units:', error); + unitSelect.innerHTML = ''; + } +} -// Write your JavaScript code. From 7976545da690deaa8be7ea4d7617581b2baf1c4c Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 11:56:15 +0200 Subject: [PATCH 04/30] Add Method for uploading Instruction Image to Controller and put ValidateAntiForgeryKey instead of AutoValidateAntiForgeryKey --- .../MediaFile/MediaFileController.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs index 1f17d7a..452bb9f 100644 --- a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -15,7 +15,7 @@ // POST: /UploadImage [HttpPost("UploadImage")] - [AutoValidateAntiforgeryToken] + [ValidateAntiForgeryToken] public async Task UploadImage(Guid recipeId, IFormFile? mediaFile) { if (mediaFile is null) @@ -42,7 +42,7 @@ } [HttpPost("ReplaceInstructionImage")] - [AutoValidateAntiforgeryToken] + [ValidateAntiForgeryToken] public async Task ReplaceInstructionImage(Guid instructionId, Guid mediaFileIdToReplace, IFormFile? newPhoto) { if (newPhoto is null) @@ -67,5 +67,26 @@ } } } + + [HttpPost("Recipe/{recipeId}/Instruction/{instructionId}/UploadImage")] + [ValidateAntiForgeryToken] + public async Task UploadInstructionImage(Guid recipeId, Guid instructionId, IFormFile? photo) + { + if (photo == null) + { + return BadRequest("Photo is required."); + } + + try + { + await _mediaFileRepository.UploadInstructionImageAsync(instructionId, photo); + + return RedirectToAction("GetInstructions", "Instruction", new { recipeId }); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } } } From 000cead8774d532bce340d849133e081b4da9e78 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:01:53 +0200 Subject: [PATCH 05/30] - Replaced `foreach` loop with `for` loop for better index-based access to instructions. - Added logic to dynamically retrieve media files for each instruction using `Request.Form.Files`. - Ensured that media files are correctly associated with their respective instructions during recipe creation. --- .../Controller/Recipe/RecipeController.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index d7abfaa..4477962 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -230,14 +230,23 @@ if (model.InstructionViewModel?.Instructions != null) { - foreach (var instruction in model.InstructionViewModel.Instructions) + for (var i = 0; i < model.InstructionViewModel.Instructions.Count; i++) { + var instruction = model.InstructionViewModel.Instructions[i]; if (!string.IsNullOrWhiteSpace(instruction.Description)) { + var fileKey = $"InstructionViewModel.Instructions[{i}].MediaFile"; + IFormFile? imageFile = null; + + if (Request.Form.Files.Any(f => f.Name == fileKey)) + { + imageFile = Request.Form.Files[fileKey]; + } + await _instructionRepository.CreateInstructionWithImageToRecipeAsync( recipe.Id, instruction.Description, - null); + imageFile); } } } From d131ec1d16fff92dc546e0e548e0b35f58207a6d Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:17:06 +0200 Subject: [PATCH 06/30] Implement a new endpoint to retrieve all units for a dynamic dropdown. --- .../Controller/Unit/UnitController.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Controller/Unit/UnitController.cs b/Francesco.Recipes.World/Controller/Unit/UnitController.cs index c0efcda..d01ffd8 100644 --- a/Francesco.Recipes.World/Controller/Unit/UnitController.cs +++ b/Francesco.Recipes.World/Controller/Unit/UnitController.cs @@ -1,6 +1,25 @@ namespace Francesco.Recipes.World.Controller.Unit { - public class UnitController + using Francesco.Recipes.World.Repositories.Unit; + using Microsoft.AspNetCore.Mvc; + + [Route("Unit")] + public class UnitController : Controller { + private readonly IUnitRepository _unitRepository; + + public UnitController(IUnitRepository unitRepository) + { + _unitRepository = unitRepository; + } + + [HttpGet("GetAllUnits")] + public async Task GetAllUnits() + { + var units = (await _unitRepository.GetAllUnitsAsync()) + .Select(u => new { id = u.Id, name = u.Name }) + .ToList(); + return Json(units); + } } } From 0d57d40651ab1375ddb541e4771411640478ba74 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:18:22 +0200 Subject: [PATCH 07/30] Make Instruction and Recipe nullable for avoid data inconsistency --- .../Models/BackendModels/MediaFile/MediaFile.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Francesco.Recipes.World/Models/BackendModels/MediaFile/MediaFile.cs b/Francesco.Recipes.World/Models/BackendModels/MediaFile/MediaFile.cs index f8a8218..6f98b14 100644 --- a/Francesco.Recipes.World/Models/BackendModels/MediaFile/MediaFile.cs +++ b/Francesco.Recipes.World/Models/BackendModels/MediaFile/MediaFile.cs @@ -13,8 +13,8 @@ public byte[]? Data { get; set; } - public virtual Recipe? Recipe { get; set; } = new (); + public virtual Recipe? Recipe { get; set; } = null; - public virtual Instruction Instruction { get; set; } = new (); + public virtual Instruction? Instruction { get; set; } = null; } } From 080c4eda7884ef0e4cf925e9511cc4dca2a69d5b Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:19:27 +0200 Subject: [PATCH 08/30] Create ViewModels for the Create Recipe logic --- .../Models/CreateRecipeViewModel.cs | 37 +++++++++++++++++++ .../Models/IngredientViewModel.cs | 14 +++++++ 2 files changed, 51 insertions(+) create mode 100644 Francesco.Recipes.World/Models/CreateRecipeViewModel.cs create mode 100644 Francesco.Recipes.World/Models/IngredientViewModel.cs diff --git a/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs b/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs new file mode 100644 index 0000000..3f15930 --- /dev/null +++ b/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs @@ -0,0 +1,37 @@ +using Francesco.Recipes.World.Models.BackendModels.Recipe; + +namespace Francesco.Recipes.World.Models +{ + public class CreateRecipeViewModel + { + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public Difficulty Difficulty { get; set; } + + public int Servings { get; set; } + + public TimeSpan PreparationTime { get; set; } + + public TimeSpan CookingTime { get; set; } + + public int PrepHours { get; set; } + + public int PrepMinutes { get; set; } + + public int CookHours { get; set; } + + public int CookMinutes { get; set; } + + public Guid CategoryId { get; set; } + + public IFormFile? Photo { get; set; } + + public IFormFile? Video { get; set; } + + public IngredientViewModel? IngredientViewModel { get; set; } + + public InstructionViewModel? InstructionViewModel { get; set; } + } +} diff --git a/Francesco.Recipes.World/Models/IngredientViewModel.cs b/Francesco.Recipes.World/Models/IngredientViewModel.cs new file mode 100644 index 0000000..bb16514 --- /dev/null +++ b/Francesco.Recipes.World/Models/IngredientViewModel.cs @@ -0,0 +1,14 @@ +namespace Francesco.Recipes.World.Models +{ + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.Unit; + + public class IngredientViewModel + { + public Guid RecipeId { get; set; } + + public List Ingredients { get; set; } = new List(); + + public List Units { get; set; } = new List(); + } +} From a76f757081c3677527fdf10c52a7c909863c0c52 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:24:20 +0200 Subject: [PATCH 09/30] - Forgive an IFormFile Parameter for CreateInstructionWithImageToRecipeAsync -Refactor instruction number generation to simplify null-check and remove redundant increment - By creating an InstructionFile put Recipe Id to null for avoid Datainconsistency --- .../Instruction/IInstructionRepository.cs | 2 +- .../Instruction/InstructionRepository.cs | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs index ef8094f..40dc4a1 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs @@ -6,7 +6,7 @@ { Task GetInstructionAsync(Guid instructionId); - Task CreateInstructionToRecipeAsync(Guid recipeId, string description); + Task CreateInstructionWithImageToRecipeAsync(Guid recipeId, string description, IFormFile? photo); Task RemoveInstructionFromRecipeAsync(Guid recipeId, Guid instructionId); diff --git a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs index 6844c0a..9f03f28 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs @@ -2,6 +2,7 @@ { using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models.BackendModels.Instruction; + using Francesco.Recipes.World.Models.BackendModels.MediaFile; using Francesco.Recipes.World.Repositories.Recipe; using Microsoft.EntityFrameworkCore; @@ -22,7 +23,7 @@ return instruction ?? throw new InvalidDataException($"Instruction {instructionId} not found."); } - public async Task CreateInstructionToRecipeAsync(Guid recipeId, string description) + public async Task CreateInstructionWithImageToRecipeAsync(Guid recipeId, string description, IFormFile? photo) { if (string.IsNullOrWhiteSpace(description)) { @@ -38,8 +39,11 @@ throw new ArgumentException("Recipe not found.", nameof(recipeId)); } - var nextNumber = recipe.Instructions?.Max(i => i.Number) ?? 0; - nextNumber++; + var nextNumber = 1; + if (recipe.Instructions != null && recipe.Instructions.Any()) + { + nextNumber = recipe.Instructions.Max(i => i.Number) + 1; + } var newInstruction = new Instruction { @@ -52,6 +56,24 @@ _context.Instructions.Add(newInstruction); await _context.SaveChangesAsync(); + if (photo != null && photo.Length > 0) + { + using var memoryStream = new MemoryStream(); + await photo.CopyToAsync(memoryStream); + + var instructionImage = new MediaFile + { + FileName = photo.FileName, + MimeType = photo.ContentType, + Data = memoryStream.ToArray(), + Instruction = newInstruction, + Recipe = null, + }; + + _context.Add(instructionImage); + await _context.SaveChangesAsync(); + } + return newInstruction; } @@ -88,6 +110,7 @@ public async Task> GetInstructionsOfRecipeAsync(Guid recipeId) { var instructions = await _context.Instructions + .Include(i => i.MediaFiles) .Where(i => i.Recipe.Id == recipeId) .OrderBy(i => i.Number) .ToListAsync(); From 1a91ec38dda1bcf0bef760107acf715fb0b2a9e0 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:26:19 +0200 Subject: [PATCH 10/30] Avoid datainconsistency --- .../Repositories/MediaFile/MediaFileRepository.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs index ef3babb..4e53ba9 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -64,6 +64,7 @@ MimeType = photo.ContentType, Data = memoryStream.ToArray(), Instruction = instruction, + Recipe = null, }; _context.Add(instructionImage); @@ -107,6 +108,7 @@ MimeType = mediaFile.ContentType, Data = memoryStream.ToArray(), Recipe = recipe, + Instruction = null, }; _context.MediaFiles.Add(newMedia); From 594315c473470b278e68d75ab6df1c4379565fec Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:29:19 +0200 Subject: [PATCH 11/30] Make link to page for create Recipe instead to show the form dynamically by button click --- Francesco.Recipes.World/Views/Home/Index.cshtml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Francesco.Recipes.World/Views/Home/Index.cshtml b/Francesco.Recipes.World/Views/Home/Index.cshtml index 38c290a..af2e530 100644 --- a/Francesco.Recipes.World/Views/Home/Index.cshtml +++ b/Francesco.Recipes.World/Views/Home/Index.cshtml @@ -71,11 +71,8 @@ From a7a5650046bafee19014290182cb2a5331b605db Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:30:53 +0200 Subject: [PATCH 12/30] Make a completly new Create view with some css style, that contains all PartialViews --- .../Views/Recipe/Create.cshtml | 220 ++++++++++++++---- 1 file changed, 175 insertions(+), 45 deletions(-) diff --git a/Francesco.Recipes.World/Views/Recipe/Create.cshtml b/Francesco.Recipes.World/Views/Recipe/Create.cshtml index e8c3769..5e3f77a 100644 --- a/Francesco.Recipes.World/Views/Recipe/Create.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -1,58 +1,188 @@ -@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe +@model Francesco.Recipes.World.Models.CreateRecipeViewModel @using Francesco.Recipes.World.Models.BackendModels.Recipe +@using Francesco.Recipes.World.Models @{ - ViewData["Title"] = "Create Recipe"; + ViewData["Title"] = "Erstelle Rezept"; } -

Create Recipe

+

Erstelle Rezept

-
-
- - - + +
+

Allgemein

+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ +
+
+ +
+ h +
+
+
+ +
+ min +
+
+
+
+
+ +
+
+ +
+ h +
+
+
+ +
+ min +
+
+
+
+
-
- - - + +
+

Zutaten

+ @await Html.PartialAsync("_IngredientsPartial", Model.IngredientViewModel ?? new IngredientViewModel +{ + RecipeId = Model.CategoryId, // Setzt die richtige ID für das globale Script + Ingredients = new List(), + Units = ViewBag.Units ?? new List() +})
-
- - - + +
+

Anweisungen

+ @await Html.PartialAsync("_GetInstructions", Model.InstructionViewModel ?? new InstructionViewModel +{ + RecipeId = Model.CategoryId, + Instructions = new List() +})
-
- - - + +
+

Bild/Video

+
+ + +
+
+ + +
-
- - - + +
+ + Abbrechen
-
- - - -
-
- - -
- -@section Scripts { - @{ - await Html.RenderPartialAsync("_ValidationScriptsPartial"); - } -} + + +@section Scripts { + + + @await Html.PartialAsync("_ValidationScriptsPartial") + +} From 1e6d354343ecff777c6c66b2c30e0496f1887d77 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:32:32 +0200 Subject: [PATCH 13/30] Remove js an placed to site.js --- .../Views/Shared/_GetInstructions.cshtml | 118 ++++-------------- .../Views/Shared/_IngredientsPartial.cshtml | 35 ++++++ 2 files changed, 56 insertions(+), 97 deletions(-) create mode 100644 Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml diff --git a/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml index 0c8a725..aa05493 100644 --- a/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml @@ -5,114 +5,38 @@ {
- - - + @if (Model.Instructions[i].MediaFiles != null && Model.Instructions[i].MediaFiles.Any()) + { + var mediaFile = Model.Instructions[i].MediaFiles.First(); + if (mediaFile.Data != null) + { +
+ Instruction Media +
+ } + } + + + +
- - + +
}
+ + + @section Scripts { } diff --git a/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml new file mode 100644 index 0000000..98670c5 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml @@ -0,0 +1,35 @@ +@model Francesco.Recipes.World.Models.IngredientViewModel +@Html.AntiForgeryToken() +
+ @for (int i = 0; i < Model.Ingredients.Count; i++) + { +
+
+ + + + +
+
+ } +
+ + + +@section Scripts { + +} From a2e50a01955826e9ed423ebf07ea4409da525d1c Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 12:33:05 +0200 Subject: [PATCH 14/30] Move all js scripts into this file --- Francesco.Recipes.World/wwwroot/js/site.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Francesco.Recipes.World/wwwroot/js/site.js b/Francesco.Recipes.World/wwwroot/js/site.js index c69a044..12bdab8 100644 --- a/Francesco.Recipes.World/wwwroot/js/site.js +++ b/Francesco.Recipes.World/wwwroot/js/site.js @@ -103,7 +103,7 @@ async function removeInstruction(instructionId, recipeIdParam) { function addInstruction() { const container = document.getElementById('instructions-container'); - const index = document.querySelectorAll('.instruction-item').length; // Index for model binding + const index = document.querySelectorAll('.instruction-item').length; const newInstructionHtml = `
@@ -150,7 +150,7 @@ async function removeIngredient(ingredientId) { async function addIngredient() { const container = document.getElementById('ingredients-container'); - const index = document.querySelectorAll('.ingredient-item').length; // Index for model binding + const index = document.querySelectorAll('.ingredient-item').length; const newIngredientHtml = `
From a0044a8d30d07ebef9755edd69d8f34506627a3e Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 17:05:30 +0200 Subject: [PATCH 15/30] Check recipeId null --- .../Controller/MediaFile/MediaFileController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs index 452bb9f..d0bfe25 100644 --- a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -72,6 +72,11 @@ [ValidateAntiForgeryToken] public async Task UploadInstructionImage(Guid recipeId, Guid instructionId, IFormFile? photo) { + if (recipeId == Guid.Empty) + { + return BadRequest("Recipe ID is required."); + } + if (photo == null) { return BadRequest("Photo is required."); From 0ff81912a93598dc0ca059586839472293b0b524 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 17:07:39 +0200 Subject: [PATCH 16/30] Rename Method from CreateInstructionWithImageToRecipeAsync to CreateInstructionAsync --- .../Repositories/Instruction/IInstructionRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs index 40dc4a1..7de7e9e 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs @@ -6,7 +6,7 @@ { Task GetInstructionAsync(Guid instructionId); - Task CreateInstructionWithImageToRecipeAsync(Guid recipeId, string description, IFormFile? photo); + Task CreateInstructionAsync(Guid recipeId, string description, IFormFile? photo); Task RemoveInstructionFromRecipeAsync(Guid recipeId, Guid instructionId); From 402f5f2fe9a498c9ec77abceb62a6809be5ff191 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 17:08:17 +0200 Subject: [PATCH 17/30] Rename method --- .../Repositories/Recipe/IRecipeRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index 63c20af..fdbc876 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -9,7 +9,7 @@ Task GetRecipeByIdAsync(Guid id); - Task AddOrCreateIngredientToRecipeAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId); + Task CreateRecipeIngredientAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId); Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId); From dc9a1f1b9cd032e2f23a9fc3fcb6b4ad6b3117c1 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 17:10:05 +0200 Subject: [PATCH 18/30] Move script section stuff that is implementet on more than one script section on layout --- .../Views/Recipe/RemoveIngredient.cshtml | 3 --- .../Views/Shared/_GetInstructions.cshtml | 6 +----- .../Views/Shared/_IngredientsPartial.cshtml | 6 +----- .../Views/Shared/_Layout.cshtml | 17 +++++++++++++---- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml b/Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml index ca32396..6e868fd 100644 --- a/Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml @@ -18,6 +18,3 @@
-@section Scripts { - @await Html.PartialAsync("_ValidationScriptsPartial") -} diff --git a/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml index aa05493..206818b 100644 --- a/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml @@ -35,8 +35,4 @@ -@section Scripts { - -} + diff --git a/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml index 98670c5..ee780e2 100644 --- a/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml @@ -28,8 +28,4 @@ -@section Scripts { - -} + diff --git a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml index dace8db..463da8d 100644 --- a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml @@ -63,7 +63,16 @@ - - + @await Html.PartialAsync("_ValidationScriptsPartial") @await RenderSectionAsync("Scripts", required: false) From 4928a3cc7da5f66e05b838b2976a7bccc8c270a9 Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 17:10:40 +0200 Subject: [PATCH 19/30] Move css style on a separate .css file --- .../Views/Recipe/Create.cshtml | 76 +------------------ Francesco.Recipes.World/wwwroot/css/site.css | 65 ++++++++++++++++ 2 files changed, 66 insertions(+), 75 deletions(-) diff --git a/Francesco.Recipes.World/Views/Recipe/Create.cshtml b/Francesco.Recipes.World/Views/Recipe/Create.cshtml index 5e3f77a..b10907b 100644 --- a/Francesco.Recipes.World/Views/Recipe/Create.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -78,7 +78,7 @@

Zutaten

@await Html.PartialAsync("_IngredientsPartial", Model.IngredientViewModel ?? new IngredientViewModel { - RecipeId = Model.CategoryId, // Setzt die richtige ID für das globale Script + RecipeId = Model.CategoryId, Ingredients = new List(), Units = ViewBag.Units ?? new List() }) @@ -111,78 +111,4 @@
- - -@section Scripts { - - - @await Html.PartialAsync("_ValidationScriptsPartial") - -} diff --git a/Francesco.Recipes.World/wwwroot/css/site.css b/Francesco.Recipes.World/wwwroot/css/site.css index f8d98fc..a505610 100644 --- a/Francesco.Recipes.World/wwwroot/css/site.css +++ b/Francesco.Recipes.World/wwwroot/css/site.css @@ -19,4 +19,69 @@ html { body { margin-bottom: 60px; +} + +.instruction-item, .ingredient-item { + background-color: #f8f9fa; + padding: 15px; + margin-bottom: 10px; + border-radius: 4px; +} + +.instruction-controls, .ingredient-controls { + display: flex; + align-items: center; + gap: 10px; +} + +.instruction-media { + width: 80px; + height: 80px; + overflow: hidden; + margin-right: 10px; +} + + .instruction-media img { + width: 100%; + height: 100%; + object-fit: cover; + } + +.instruction-file { + max-width: 200px; +} + +textarea.form-control { + min-height: 80px; +} + +.btn-delete, .btn-move-up, .btn-move-down, .btn-save { + background: none; + border: none; + font-size: 1.2rem; + cursor: pointer; +} + +.btn-delete { + color: #dc3545; +} + +.btn-save { + color: #28a745; +} + +.instruction-actions { + display: flex; + justify-content: flex-end; + margin-top: 5px; +} + +.btn-add { + background-color: #007bff; + color: white; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + margin-top: 10px; } \ No newline at end of file From a0d05dcdc2d9d5740b44b6c1a3e77beff7f7da1e Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 17:11:18 +0200 Subject: [PATCH 20/30] change from ienumerable to list --- Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs | 2 +- Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs b/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs index a8244cd..8ce5267 100644 --- a/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs +++ b/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs @@ -8,6 +8,6 @@ Task AddUnitAsync(string name, string symbol); - Task> GetAllUnitsAsync(); + Task> GetAllUnitsAsync(); } } diff --git a/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs b/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs index 04297cd..6d6a800 100644 --- a/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs +++ b/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs @@ -35,7 +35,7 @@ return unit; } - public async Task> GetAllUnitsAsync() + public async Task> GetAllUnitsAsync() { return await _context.Units.ToListAsync(); } From 7168d936216a3b4777f2726767fdc5afabacf27b Mon Sep 17 00:00:00 2001 From: franc Date: Mon, 5 May 2025 17:12:06 +0200 Subject: [PATCH 21/30] Rename Methode --- .../Repositories/Instruction/InstructionRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs index 9f03f28..aed5d2a 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs @@ -23,7 +23,7 @@ return instruction ?? throw new InvalidDataException($"Instruction {instructionId} not found."); } - public async Task CreateInstructionWithImageToRecipeAsync(Guid recipeId, string description, IFormFile? photo) + public async Task CreateInstructionAsync(Guid recipeId, string description, IFormFile? photo) { if (string.IsNullOrWhiteSpace(description)) { From abb1d774feb284236c909a56c78a0b397acd44d6 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 7 May 2025 08:53:12 +0200 Subject: [PATCH 22/30] Solve all thread from mergeRequest to RecipeController --- .../Controller/Recipe/RecipeController.cs | 206 ++++++++++-------- 1 file changed, 120 insertions(+), 86 deletions(-) diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 4477962..d9b6996 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -2,6 +2,8 @@ { using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models; + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.Instruction; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Category; using Francesco.Recipes.World.Repositories.Favorit; @@ -14,7 +16,6 @@ using Francesco.Recipes.World.Views.Recipe; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; - using Microsoft.EntityFrameworkCore; public class RecipeController : Controller { @@ -101,6 +102,11 @@ [ValidateAntiForgeryToken] public async Task AddOrCreateIngredient(Guid recipeId, string ingredientName, int quantity, Guid unitId) { + if (recipeId == Guid.Empty) + { + return BadRequest("Recipe ID cannot be empty."); + } + if (quantity <= 0) { ModelState.AddModelError(nameof(quantity), "Die Menge muss größer als 0 sein."); @@ -113,7 +119,7 @@ return View(); } - await _recipeRepository.AddOrCreateIngredientToRecipeAsync(recipeId, ingredientName, quantity, unitId); + await _recipeRepository.CreateRecipeIngredientAsync(recipeId, ingredientName, quantity, unitId); return RedirectToAction("Details", new { id = recipeId }); } @@ -128,32 +134,30 @@ } var units = await _unitRepository.GetAllUnitsAsync(); - ViewBag.Units = new SelectList(units, "Id", "Name"); var viewModel = new CreateRecipeViewModel { CategoryId = categoryId, + CategoryName = category.Name, IngredientViewModel = new IngredientViewModel { RecipeId = Guid.Empty, - Ingredients = new List(), + Ingredients = new List(), Units = units.ToList(), }, InstructionViewModel = new InstructionViewModel { RecipeId = Guid.Empty, - Instructions = new List(), + Instructions = new List(), }, }; - - ViewBag.CategoryName = category.Name; return View(viewModel); } // POST: /Recipe/Create/{categoryId} [HttpPost("Create/{categoryId}")] [ValidateAntiForgeryToken] - public async Task Create(Guid categoryId, CreateRecipeViewModel model, IFormFile? photo, IFormFile? video) + public async Task Create(Guid categoryId, CreateRecipeViewModel model) { if (model == null) { @@ -179,79 +183,124 @@ } var units = await _unitRepository.GetAllUnitsAsync(); - ViewBag.Units = new SelectList(units, "Id", "Name"); - ViewBag.CategoryName = category.Name; + + if (model.IngredientViewModel == null) + { + model.IngredientViewModel = new IngredientViewModel + { + RecipeId = Guid.Empty, + Ingredients = new List(), + Units = units.ToList(), + }; + } + else + { + model.IngredientViewModel.Units = units.ToList(); + } + + model.CategoryName = category.Name; return View(model); } - var categoryEntity = await _categoryRepository.GetCategoryByIdAsync(categoryId); - if (categoryEntity == null) + using var transaction = await _context.Database.BeginTransactionAsync(); + try { - return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); - } - - model.PreparationTime = new TimeSpan(model.PrepHours, model.PrepMinutes, 0); - model.CookingTime = new TimeSpan(model.CookHours, model.CookMinutes, 0); - - var recipe = await _recipeRepository.CreateRecipeForCategoryAsync( - categoryEntity, - model.Name, - model.Description ?? string.Empty, - model.Difficulty, - model.Servings, - model.PreparationTime, - model.CookingTime); - - if (photo != null) - { - await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, photo); - } - - if (video != null) - { - await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, video); - } - - if (model.IngredientViewModel?.Ingredients != null) - { - foreach (var ingredient in model.IngredientViewModel.Ingredients) + var categoryEntity = await _categoryRepository.GetCategoryByIdAsync(categoryId); + if (categoryEntity == null) { - var ri = ingredient.RecipeIngredients?.FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(ingredient.Name) && ri?.Quantity > 0 && ri?.Unit?.Id != null) - { - await _recipeRepository.AddOrCreateIngredientToRecipeAsync( - recipe.Id, - ingredient.Name, - ri.Quantity, - ri.Unit.Id); - } + return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } - } - if (model.InstructionViewModel?.Instructions != null) - { - for (var i = 0; i < model.InstructionViewModel.Instructions.Count; i++) + model.PreparationTime = new TimeSpan(model.PrepHours, model.PrepMinutes, 0); + model.CookingTime = new TimeSpan(model.CookHours, model.CookMinutes, 0); + + var recipe = await _recipeRepository.CreateRecipeForCategoryAsync( + categoryEntity, + model.Name, + model.Description ?? string.Empty, + model.Difficulty, + model.Servings, + model.PreparationTime, + model.CookingTime); + + if (model.Photo != null) { - var instruction = model.InstructionViewModel.Instructions[i]; - if (!string.IsNullOrWhiteSpace(instruction.Description)) - { - var fileKey = $"InstructionViewModel.Instructions[{i}].MediaFile"; - IFormFile? imageFile = null; + await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, model.Photo); + } - if (Request.Form.Files.Any(f => f.Name == fileKey)) + if (model.Video != null) + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, model.Video); + } + + if (model.IngredientViewModel?.Ingredients != null) + { + foreach (var ingredient in model.IngredientViewModel.Ingredients) + { + var ri = ingredient.RecipeIngredients?.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(ingredient.Name) && ri?.Quantity > 0 && ri?.Unit?.Id != null) { - imageFile = Request.Form.Files[fileKey]; + await _recipeRepository.CreateRecipeIngredientAsync( + recipe.Id, + ingredient.Name, + ri.Quantity, + ri.Unit.Id); } - - await _instructionRepository.CreateInstructionWithImageToRecipeAsync( - recipe.Id, - instruction.Description, - imageFile); } } - } - return RedirectToAction("Details", new { recipeId = recipe.Id }); + if (model.InstructionViewModel?.Instructions != null) + { + for (var i = 0; i < model.InstructionViewModel.Instructions.Count; i++) + { + var instruction = model.InstructionViewModel.Instructions[i]; + if (!string.IsNullOrWhiteSpace(instruction.Description)) + { + var fileKey = $"InstructionViewModel.Instructions[{i}].MediaFile"; + IFormFile? imageFile = null; + + if (Request.Form.Files.Any(f => f.Name == fileKey)) + { + imageFile = Request.Form.Files[fileKey]; + } + + await _instructionRepository.CreateInstructionAsync( + recipe.Id, + instruction.Description, + imageFile); + } + } + } + + await transaction.CommitAsync(); + return RedirectToAction("Details", new { recipeId = recipe.Id }); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + + ModelState.AddModelError(string.Empty, $"Ein Fehler ist aufgetreten beim Erstellen des Rezepts: {ex.Message}"); + + var category = await _categoryRepository.GetCategoryByIdAsync(categoryId); + var units = await _unitRepository.GetAllUnitsAsync(); + + if (model.IngredientViewModel == null) + { + model.IngredientViewModel = new IngredientViewModel + { + RecipeId = Guid.Empty, + Ingredients = new List(), + Units = units.ToList(), + }; + } + else + { + model.IngredientViewModel.Units = units.ToList(); + } + + model.CategoryName = category?.Name ?? string.Empty; + return View(model); + } } // GET: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} @@ -346,11 +395,7 @@ return NotFound("Recipe not found."); } - var instructions = await _context.Instructions - .Include(i => i.MediaFiles) - .Where(i => i.Recipe.Id == recipeId) - .OrderBy(i => i.Number) - .ToListAsync(); + var instructions = await _instructionRepository.GetInstructionsOfRecipeAsync(recipeId); var viewModel = new InstructionViewModel { @@ -368,7 +413,7 @@ { try { - await _instructionRepository.CreateInstructionWithImageToRecipeAsync(recipeId, description, image); + await _instructionRepository.CreateInstructionAsync(recipeId, description, image); var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); var instructions = recipe?.Instructions?.ToList() ?? new List(); @@ -383,18 +428,7 @@ } catch (Exception ex) { - ModelState.AddModelError(string.Empty, ex.Message); - - var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); - var instructions = recipe?.Instructions?.ToList() ?? new List(); - - var viewModel = new InstructionViewModel - { - RecipeId = recipeId, - Instructions = instructions, - }; - - return View(viewModel); + return BadRequest(ex.Message); } } @@ -441,7 +475,7 @@ public async Task GetIngredients(Guid recipeId) { var recipeIngredients = (await _ingredientRepository.GetIngredientsByRecipeIdAsync(recipeId)).Select(ri => ri.Ingredient).ToList(); - var units = (await _unitRepository.GetAllUnitsAsync()).ToList(); + var units = await _unitRepository.GetAllUnitsAsync(); var viewModel = new IngredientViewModel { From d172642b1ebedf776b51df4d16b6cd397506603f Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 7 May 2025 08:54:18 +0200 Subject: [PATCH 23/30] Add an new Attribut --- Francesco.Recipes.World/Models/CreateRecipeViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs b/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs index 3f15930..bccb60a 100644 --- a/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs +++ b/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs @@ -26,6 +26,8 @@ namespace Francesco.Recipes.World.Models public Guid CategoryId { get; set; } + public string? CategoryName { get; set; } + public IFormFile? Photo { get; set; } public IFormFile? Video { get; set; } From 070df12049b89521d72162631425742634b3a39c Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 7 May 2025 08:55:04 +0200 Subject: [PATCH 24/30] Add new Attribut --- Francesco.Recipes.World/Models/InstructionViewModel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Francesco.Recipes.World/Models/InstructionViewModel.cs b/Francesco.Recipes.World/Models/InstructionViewModel.cs index 28c6911..fc5afda 100644 --- a/Francesco.Recipes.World/Models/InstructionViewModel.cs +++ b/Francesco.Recipes.World/Models/InstructionViewModel.cs @@ -1,11 +1,13 @@ -namespace Francesco.Recipes.World.Models -{ - using Francesco.Recipes.World.Models.BackendModels.Instruction; +using Francesco.Recipes.World.Models.BackendModels.Instruction; +namespace Francesco.Recipes.World.Models +{ public class InstructionViewModel { public Guid RecipeId { get; set; } + public string Description { get; set; } = string.Empty; + public List Instructions { get; set; } = new List(); } } From 7bf43b1855886d78fc65cf029f6fbe918f836268 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 7 May 2025 08:55:40 +0200 Subject: [PATCH 25/30] Change some things --- .../MediaFile/MediaFileRepository.cs | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs index 4e53ba9..d4a8d9a 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -74,45 +74,52 @@ public async Task UploadRecipeMediaAsync(Guid recipeId, IFormFile mediaFile) { - if (mediaFile == null) + if (mediaFile == null || mediaFile.Length == 0) { - throw new ArgumentNullException(nameof(mediaFile)); + return; } - var recipe = await _recipeRepository.GetRecipeAsync(recipeId); - - var isImage = mediaFile.ContentType.StartsWith("image/"); - var isVideo = mediaFile.ContentType.StartsWith("video/"); - - if (!isImage && !isVideo) + try { - throw new InvalidOperationException("Only image or video files are allowed."); + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + + var isImage = mediaFile.ContentType.StartsWith("image/"); + var isVideo = mediaFile.ContentType.StartsWith("video/"); + + if (!isImage && !isVideo) + { + throw new InvalidOperationException("Only image or video files are allowed."); + } + + if (isImage) + { + await RemoveExistingMediaAsync(recipe, "image/"); + } + else + { + await RemoveExistingMediaAsync(recipe, "video/"); + } + + using var memoryStream = new MemoryStream(); + await mediaFile.CopyToAsync(memoryStream); + + var newMedia = new MediaFile + { + Id = Guid.NewGuid(), + FileName = mediaFile.FileName, + MimeType = mediaFile.ContentType, + Data = memoryStream.ToArray(), + Recipe = recipe, + Instruction = null, + }; + + _context.MediaFiles.Add(newMedia); + await _context.SaveChangesAsync(); } - - if (isImage) + catch (Exception ex) { - await RemoveExistingMediaAsync(recipe, "image/"); + throw new InvalidOperationException("An error occurred while uploading the media file.", ex); } - else - { - await RemoveExistingMediaAsync(recipe, "video/"); - } - - using var memoryStream = new MemoryStream(); - await mediaFile.CopyToAsync(memoryStream); - - var newMedia = new MediaFile - { - Id = Guid.NewGuid(), - FileName = mediaFile.FileName, - MimeType = mediaFile.ContentType, - Data = memoryStream.ToArray(), - Recipe = recipe, - Instruction = null, - }; - - _context.MediaFiles.Add(newMedia); - await _context.SaveChangesAsync(); } private async Task RemoveExistingMediaAsync(Recipe recipe, string mediaTypePrefix) From 12b217ea5905dae6a022e9dc18e0404e0fa90c98 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 7 May 2025 08:56:00 +0200 Subject: [PATCH 26/30] Rename Method --- Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index c0f6b27..382bf90 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -41,7 +41,7 @@ .FirstOrDefaultAsync(r => r.Id == recipeId); } - public async Task AddOrCreateIngredientToRecipeAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId) + public async Task CreateRecipeIngredientAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId) { var recipe = await GetRecipeAsync(recipeId); var unit = await _unitRepository.GetUnitByIdAsync(unitId); From a7cafb479b2b61379a09faac3f30d59a321c5435 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 7 May 2025 08:59:23 +0200 Subject: [PATCH 27/30] Use normal List instead interface list --- Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs | 2 +- Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs b/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs index 8ce5267..5be0996 100644 --- a/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs +++ b/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs @@ -8,6 +8,6 @@ Task AddUnitAsync(string name, string symbol); - Task> GetAllUnitsAsync(); + Task> GetAllUnitsAsync(); } } diff --git a/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs b/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs index 6d6a800..29a3807 100644 --- a/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs +++ b/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs @@ -35,7 +35,7 @@ return unit; } - public async Task> GetAllUnitsAsync() + public async Task> GetAllUnitsAsync() { return await _context.Units.ToListAsync(); } From 03932b380de58fda836a14885caf5ecaf68f5750 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 7 May 2025 08:59:43 +0200 Subject: [PATCH 28/30] change some things --- Francesco.Recipes.World/Views/Shared/_Layout.cshtml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml index 463da8d..a06f9bd 100644 --- a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml @@ -63,16 +63,18 @@