diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs index 1f17d7a..d0bfe25 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,31 @@ } } } + + [HttpPost("Recipe/{recipeId}/Instruction/{instructionId}/UploadImage")] + [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."); + } + + try + { + await _mediaFileRepository.UploadInstructionImageAsync(instructionId, photo); + + return RedirectToAction("GetInstructions", "Instruction", new { recipeId }); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } } } diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 72bf903..e19eea3 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -1,5 +1,9 @@ namespace Francesco.Recipes.World.Controller.Recipe { + 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; @@ -22,6 +26,7 @@ private readonly IMediaFileRepository _mediaFileRepository; private readonly IInstructionRepository _instructionRepository; private readonly IFavoriteRepository _favoriteRepository; + private readonly FrancescosRecipesWorldDbContext _context; public IReadOnlyCollection Recipes { get; set; } @@ -32,7 +37,8 @@ IIngredientRepository ingredientRepository, IMediaFileRepository mediaFileRepository, IInstructionRepository instructionRepository, - IFavoriteRepository favoriteRepository) + IFavoriteRepository favoriteRepository, + FrancescosRecipesWorldDbContext context) { _recipeRepository = recipeRepository; _unitRepository = unitRepository; @@ -42,16 +48,30 @@ _mediaFileRepository = mediaFileRepository; _instructionRepository = instructionRepository; _favoriteRepository = favoriteRepository; + _context = context; } // GET: /Recipe/{recipeId}/AddOrCreateIngredient [HttpGet("{recipeId}/AddOrCreateIngredient")] public async Task AddOrCreateIngredient(Guid recipeId) { + if (recipeId == Guid.Empty) + { + return BadRequest("Recipe ID cannot be empty."); + } + 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} @@ -87,6 +107,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."); @@ -99,7 +124,7 @@ return View(); } - await _recipeRepository.AddOrCreateIngredientToRecipeAsync(recipeId, ingredientName, quantity, unitId); + await _recipeRepository.CreateRecipeIngredientAsync(recipeId, ingredientName, quantity, unitId); return RedirectToAction("Details", new { id = recipeId }); } @@ -113,23 +138,45 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } - ViewBag.CategoryName = category.Name; - return View(); + var units = await _unitRepository.GetAllUnitsAsync(); + + var viewModel = new CreateRecipeViewModel + { + CategoryId = categoryId, + CategoryName = category.Name, + IngredientViewModel = new IngredientViewModel + { + RecipeId = Guid.Empty, + Ingredients = new List(), + Units = units.ToList(), + }, + InstructionViewModel = new InstructionViewModel + { + RecipeId = Guid.Empty, + Instructions = new List(), + }, + }; + 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) { - 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,24 +187,104 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } - ViewBag.CategoryName = category.Name; - return View(); + 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; + 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."); - } + var categoryEntity = await _categoryRepository.GetCategoryByIdAsync(categoryId); + if (categoryEntity == null) + { + 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); - if (photo != null) + 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) + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, model.Photo); + } + + 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) + { + await _recipeRepository.CreateRecipeIngredientAsync( + recipe.Id, + ingredient.Name, + ri.Quantity, + ri.Unit.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 _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, photo); + await transaction.RollbackAsync(); + return BadRequest(ex.Message); } - - return RedirectToAction("Details", "Category", new { id = categoryId }); } // GET: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} @@ -252,26 +379,40 @@ return NotFound("Recipe not found."); } - ViewBag.RecipeId = recipeId; - return View(recipe); + var instructions = await _instructionRepository.GetInstructionsOfRecipeAsync(recipeId); + + 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.CreateInstructionAsync(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); + return BadRequest(ex.Message); } } @@ -312,5 +453,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(); + + var viewModel = new IngredientViewModel + { + RecipeId = recipeId, + Ingredients = recipeIngredients, + Units = units, + }; + + return PartialView("_IngredientsPartial", viewModel); + } } } 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); + } } } 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 => 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; } } diff --git a/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs b/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs new file mode 100644 index 0000000..bccb60a --- /dev/null +++ b/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs @@ -0,0 +1,39 @@ +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 string? CategoryName { 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(); + } +} 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(); } } diff --git a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs index ef8094f..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 CreateInstructionToRecipeAsync(Guid recipeId, string description); + Task CreateInstructionAsync(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..aed5d2a 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 CreateInstructionAsync(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(); diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs index ef3babb..d4a8d9a 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); @@ -73,44 +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, - }; - - _context.MediaFiles.Add(newMedia); - await _context.SaveChangesAsync(); } private async Task RemoveExistingMediaAsync(Recipe recipe, string mediaTypePrefix) 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); 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); diff --git a/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs b/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs index a8244cd..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 04297cd..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(); } 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 @@ diff --git a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml deleted file mode 100644 index 1232469..0000000 --- a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml +++ /dev/null @@ -1,27 +0,0 @@ -@{ - ViewData["Title"] = "Add or Create Ingredient to Recipe"; -} - -

@ViewData["Title"]

- -
- -
- - -
-
- - -
-
- - -
- -
- -@section Scripts { - -} - diff --git a/Francesco.Recipes.World/Views/Recipe/Create.cshtml b/Francesco.Recipes.World/Views/Recipe/Create.cshtml index e8c3769..b10907b 100644 --- a/Francesco.Recipes.World/Views/Recipe/Create.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -1,58 +1,114 @@ -@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, + 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"); - } -} 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 0c8a725..206818b 100644 --- a/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml @@ -5,114 +5,34 @@ {
- - - + @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..ee780e2 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml @@ -0,0 +1,31 @@ +@model Francesco.Recipes.World.Models.IngredientViewModel +@Html.AntiForgeryToken() +
+ @for (int i = 0; i < Model.Ingredients.Count; i++) + { +
+
+ + + + +
+
+ } +
+ + + + diff --git a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml index dace8db..a06f9bd 100644 --- a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml @@ -63,7 +63,18 @@ - - + @await Html.PartialAsync("_ValidationScriptsPartial") @await RenderSectionAsync("Scripts", required: false) 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 diff --git a/Francesco.Recipes.World/wwwroot/js/site.js b/Francesco.Recipes.World/wwwroot/js/site.js index 0937657..12bdab8 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; + + 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; + + 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.