Merge branch 'feature/Create-Recipe' into 'develop'
Create-Recipe See merge request francesco.damico/francescos.recipes.world!14
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
|
||||
// POST: /UploadImage
|
||||
[HttpPost("UploadImage")]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> UploadImage(Guid recipeId, IFormFile? mediaFile)
|
||||
{
|
||||
if (mediaFile is null)
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
[HttpPost("ReplaceInstructionImage")]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> 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<IActionResult> 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Recipe> 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<IActionResult> 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<IActionResult> 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<Ingredient>(),
|
||||
Units = units.ToList(),
|
||||
},
|
||||
InstructionViewModel = new InstructionViewModel
|
||||
{
|
||||
RecipeId = Guid.Empty,
|
||||
Instructions = new List<Instruction>(),
|
||||
},
|
||||
};
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
// POST: /Recipe/Create/{categoryId}
|
||||
[HttpPost("Create/{categoryId}")]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public async Task<IActionResult> Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime, IFormFile? photo)
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> 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<Ingredient>(),
|
||||
Units = units.ToList(),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
model.IngredientViewModel.Units = units.ToList();
|
||||
}
|
||||
|
||||
model.CategoryName = category.Name;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
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, photo);
|
||||
await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, model.Photo);
|
||||
}
|
||||
|
||||
return RedirectToAction("Details", "Category", new { id = categoryId });
|
||||
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 transaction.RollbackAsync();
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// 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<IActionResult> AddInstruction(Guid recipeId, string description)
|
||||
public async Task<IActionResult> 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<Instruction>();
|
||||
|
||||
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<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IActionResult> GetAllUnits()
|
||||
{
|
||||
var units = (await _unitRepository.GetAllUnitsAsync())
|
||||
.Select(u => new { id = u.Id, name = u.Name })
|
||||
.ToList();
|
||||
return Json(units);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+571
@@ -0,0 +1,571 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Favorits");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Ingredients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("IngredientId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsChecked")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<Guid>("RecipeIngredientId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("RecipeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RecipeId");
|
||||
|
||||
b.ToTable("Instructions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<byte[]>("Data")
|
||||
.HasColumnType("varbinary(max)");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("InstructionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<TimeSpan>("CookingTime")
|
||||
.HasColumnType("time");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Difficulty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("FavoritId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsFavorite")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("ModifiedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<TimeSpan>("PreparationTime")
|
||||
.HasColumnType("time");
|
||||
|
||||
b.Property<int>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("IngredientId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("RecipeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("RecipeId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ModifiedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ShoppingLists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Francesco.Recipes.World.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakeInstructionOptionalInMediaFile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>(
|
||||
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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<Guid>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ namespace Francesco.Recipes.World.Migrations
|
||||
b.Property<string>("FileName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("InstructionId")
|
||||
b.Property<Guid?>("InstructionId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("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 =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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<Ingredient> Ingredients { get; set; } = new List<Ingredient>();
|
||||
|
||||
public List<Unit> Units { get; set; } = new List<Unit>();
|
||||
}
|
||||
}
|
||||
@@ -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<Instruction> Instructions { get; set; } = new List<Instruction>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{
|
||||
Task<Instruction> GetInstructionAsync(Guid instructionId);
|
||||
|
||||
Task<Instruction> CreateInstructionToRecipeAsync(Guid recipeId, string description);
|
||||
Task<Instruction> CreateInstructionAsync(Guid recipeId, string description, IFormFile? photo);
|
||||
|
||||
Task RemoveInstructionFromRecipeAsync(Guid recipeId, Guid instructionId);
|
||||
|
||||
|
||||
@@ -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<Instruction> CreateInstructionToRecipeAsync(Guid recipeId, string description)
|
||||
public async Task<Instruction> 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<List<Instruction>> GetInstructionsOfRecipeAsync(Guid recipeId)
|
||||
{
|
||||
var instructions = await _context.Instructions
|
||||
.Include(i => i.MediaFiles)
|
||||
.Where(i => i.Recipe.Id == recipeId)
|
||||
.OrderBy(i => i.Number)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
MimeType = photo.ContentType,
|
||||
Data = memoryStream.ToArray(),
|
||||
Instruction = instruction,
|
||||
Recipe = null,
|
||||
};
|
||||
|
||||
_context.Add(instructionImage);
|
||||
@@ -73,11 +74,13 @@
|
||||
|
||||
public async Task UploadRecipeMediaAsync(Guid recipeId, IFormFile mediaFile)
|
||||
{
|
||||
if (mediaFile == null)
|
||||
if (mediaFile == null || mediaFile.Length == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mediaFile));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var recipe = await _recipeRepository.GetRecipeAsync(recipeId);
|
||||
|
||||
var isImage = mediaFile.ContentType.StartsWith("image/");
|
||||
@@ -107,11 +110,17 @@
|
||||
MimeType = mediaFile.ContentType,
|
||||
Data = memoryStream.ToArray(),
|
||||
Recipe = recipe,
|
||||
Instruction = null,
|
||||
};
|
||||
|
||||
_context.MediaFiles.Add(newMedia);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("An error occurred while uploading the media file.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveExistingMediaAsync(Recipe recipe, string mediaTypePrefix)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
Task<Recipe?> 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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
Task<Unit> AddUnitAsync(string name, string symbol);
|
||||
|
||||
Task<IEnumerable<Unit>> GetAllUnitsAsync();
|
||||
Task<List<Unit>> GetAllUnitsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
return unit;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Unit>> GetAllUnitsAsync()
|
||||
public async Task<List<Unit>> GetAllUnitsAsync()
|
||||
{
|
||||
return await _context.Units.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -71,11 +71,8 @@
|
||||
<div class="col-md-3 mb-4">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<a href="/Recipe/Create?categoryId=@category.Category.Id"
|
||||
class="btn btn-outline-primary"
|
||||
hx-get="/Recipe/Create?categoryId=@category.Category.Id"
|
||||
hx-target="#category-@category.Category.Id"
|
||||
hx-swap="beforeend">Rezept hinzufügen</a>
|
||||
<a href="/Create/@category.Category.Id"
|
||||
class="btn btn-outline-primary">Rezept hinzufügen</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
@{
|
||||
ViewData["Title"] = "Add or Create Ingredient to Recipe";
|
||||
}
|
||||
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
|
||||
<form asp-action="AddOrCreateIngredient" method="post">
|
||||
<input type="hidden" id="RecipeId" name="recipeId" value="@ViewBag.RecipeId" />
|
||||
<div class="form-group">
|
||||
<label for="IngredientName" class="control-label">Ingredient Name</label>
|
||||
<input type="text" id="IngredientName" name="ingredientName" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="Quantity" class="control-label">Quantity</label>
|
||||
<input type="number" id="Quantity" name="quantity" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="UnitId" class="control-label">Unit</label>
|
||||
<select id="UnitId" name="unitId" class="form-control" asp-items="ViewBag.Units"></select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Add Ingredient</button>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
@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";
|
||||
}
|
||||
|
||||
<h1>Create Recipe</h1>
|
||||
<h1 class="mb-4">Erstelle Rezept</h1>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<form method="post" enctype="multipart/form-data" asp-controller="Recipe" asp-action="Create" asp-route-categoryId="@Model.CategoryId">
|
||||
<div class="mb-4">
|
||||
<h2>Allgemein</h2>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<label asp-for="Name">Name</label>
|
||||
<input asp-for="Name" class="form-control" required />
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Description"></label>
|
||||
<label asp-for="Description">Beschreibung</label>
|
||||
<textarea asp-for="Description" class="form-control"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Difficulty"></label>
|
||||
<select asp-for="Difficulty" class="form-control">
|
||||
<option value="">Select Difficulty</option>
|
||||
<label asp-for="Difficulty">Schwierigkeit</label>
|
||||
<select asp-for="Difficulty" class="form-control" required>
|
||||
<option value="">Schwierigkeit wählen</option>
|
||||
@foreach (var difficulty in Enum.GetValues(typeof(Difficulty)))
|
||||
{
|
||||
<option value="@difficulty">@difficulty</option>
|
||||
@@ -28,31 +31,84 @@
|
||||
</select>
|
||||
<span asp-validation-for="Difficulty" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Servings"></label>
|
||||
<input asp-for="Servings" class="form-control" />
|
||||
<div class="form-row">
|
||||
<div class="col-sm-4">
|
||||
<label asp-for="Servings">Portion (pro Person)</label>
|
||||
<input asp-for="Servings" type="number" min="1" class="form-control" required />
|
||||
<span asp-validation-for="Servings" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label>Vorbereitungszeit</label>
|
||||
<div class="d-flex">
|
||||
<div class="input-group mr-2">
|
||||
<input asp-for="PrepHours" type="number" class="form-control" min="0" value="0" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">h</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input asp-for="PrepMinutes" type="number" class="form-control" min="0" max="59" value="0" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label>Kochzeit</label>
|
||||
<div class="d-flex">
|
||||
<div class="input-group mr-2">
|
||||
<input asp-for="CookHours" type="number" class="form-control" min="0" value="0" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">h</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input asp-for="CookMinutes" type="number" class="form-control" min="0" max="59" value="0" />
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h2>Zutaten</h2>
|
||||
@await Html.PartialAsync("_IngredientsPartial", Model.IngredientViewModel ?? new IngredientViewModel
|
||||
{
|
||||
RecipeId = Model.CategoryId,
|
||||
Ingredients = new List<Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient>(),
|
||||
Units = ViewBag.Units ?? new List<Francesco.Recipes.World.Models.BackendModels.Unit.Unit>()
|
||||
})
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h2>Anweisungen</h2>
|
||||
@await Html.PartialAsync("_GetInstructions", Model.InstructionViewModel ?? new InstructionViewModel
|
||||
{
|
||||
RecipeId = Model.CategoryId,
|
||||
Instructions = new List<Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction>()
|
||||
})
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h2>Bild/Video</h2>
|
||||
<div class="form-group">
|
||||
<label asp-for="PreparationTime"></label>
|
||||
<input asp-for="PreparationTime" class="form-control" />
|
||||
<span asp-validation-for="PreparationTime" class="text-danger"></span>
|
||||
<label asp-for="Photo">Foto</label>
|
||||
<input asp-for="Photo" class="form-control-file" accept="image/*" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CookingTime"></label>
|
||||
<input asp-for="CookingTime" class="form-control" />
|
||||
<span asp-validation-for="CookingTime" class="text-danger"></span>
|
||||
<label asp-for="Video">Video</label>
|
||||
<input asp-for="Video" class="form-control-file" accept="video/*" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="photo">Photo</label>
|
||||
<input type="file" name="photo" class="form-control" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<button type="submit" class="btn btn-success btn-lg">Rezept speichern</button>
|
||||
<a asp-action="Index" asp-controller="Category" class="btn btn-secondary btn-lg ml-2">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
@{
|
||||
await Html.RenderPartialAsync("_ValidationScriptsPartial");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,3 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
|
||||
@@ -5,114 +5,34 @@
|
||||
{
|
||||
<div class="instruction-item" id="instruction-@Model.Instructions[i].Id">
|
||||
<div class="instruction-controls">
|
||||
<input type="file" />
|
||||
<textarea placeholder="Beschreibung" class="form-control">@Model.Instructions[i].Description</textarea>
|
||||
<button type="button" class="btn-delete" onclick="removeInstruction('@Model.Instructions[i].Id')">🗑️</button>
|
||||
@if (Model.Instructions[i].MediaFiles != null && Model.Instructions[i].MediaFiles.Any())
|
||||
{
|
||||
var mediaFile = Model.Instructions[i].MediaFiles.First();
|
||||
if (mediaFile.Data != null)
|
||||
{
|
||||
<div class="instruction-media">
|
||||
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)"
|
||||
alt="Instruction Media" class="instruction-media-preview" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<input type="file" name="InstructionViewModel.Instructions[@i].MediaFile" class="instruction-file" />
|
||||
<textarea name="InstructionViewModel.Instructions[@i].Description" placeholder="Beschreibung" class="form-control">@Model.Instructions[i].Description</textarea>
|
||||
|
||||
<button type="button" class="btn-delete" onclick="removeInstruction('@Model.Instructions[i].Id', '@Model.RecipeId',)">🗑️</button>
|
||||
</div>
|
||||
<div class="instruction-actions">
|
||||
<button type="button" class="btn-move-up" onclick="moveInstructionUp('@Model.Instructions[i].Id')">⬆️</button>
|
||||
<button type="button" class="btn-move-down" onclick="moveInstructionDown('@Model.Instructions[i].Id')">⬇️</button>
|
||||
<button type="button" class="btn-move-up" onclick="moveInstructionUp('@Model.Instructions[i].Id', '@Model.RecipeId')">⬆️</button>
|
||||
<button type="button" class="btn-move-down" onclick="moveInstructionDown('@Model.Instructions[i].Id', '@Model.RecipeId')">⬇️</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<button type="button" class="btn-add" onclick="addInstruction()">Schritte hinzufügen</button>
|
||||
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
htmx.on('htmx:afterSwap', (event) => {
|
||||
if (event.target.id === 'instructions-container') {
|
||||
console.log('Instructions reloaded.');
|
||||
}
|
||||
});
|
||||
|
||||
const recipeId = '@Model.RecipeId';
|
||||
|
||||
|
||||
async function moveInstructionUp(instructionId) {
|
||||
try {
|
||||
const response = await fetch(`/Recipe/${recipeId}/Instruction/${instructionId}/move-up`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
htmx.ajax('GET', `/Recipe/${recipeId}/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) {
|
||||
try {
|
||||
const response = await fetch(`/Recipe/${recipeId}/Instruction/${instructionId}/move-down`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
htmx.ajax('GET', `/Recipe/${recipeId}/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) {
|
||||
if (!confirm('Möchtest du diesen Schritt wirklich löschen?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/${recipeId}/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 newInstructionHtml = `
|
||||
<div class="instruction-item">
|
||||
<div class="instruction-controls">
|
||||
<input type="file" />
|
||||
<textarea placeholder="Beschreibung" class="form-control"></textarea>
|
||||
<button type="button" class="btn-delete" onclick="deleteInstruction()">🗑️</button>
|
||||
</div>
|
||||
<div class="instruction-actions">
|
||||
<button type="button" class="btn-move-up" onclick="moveInstructionUp()">⬆️</button>
|
||||
<button type="button" class="btn-move-down" onclick="moveInstructionDown()">⬇️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', newInstructionHtml);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
@model Francesco.Recipes.World.Models.IngredientViewModel
|
||||
@Html.AntiForgeryToken()
|
||||
<div id="ingredients-container">
|
||||
@for (int i = 0; i < Model.Ingredients.Count; i++)
|
||||
{
|
||||
<div class="ingredient-item" id="ingredient-@Model.Ingredients[i].Id">
|
||||
<div class="ingredient-controls">
|
||||
<input type="text" name="IngredientViewModel.Ingredients[@i].Name" value="@Model.Ingredients[i].Name" placeholder="Name" class="form-control" />
|
||||
<input type="number" name="IngredientViewModel.Ingredients[@i].RecipeIngredients[0].Quantity" value="@(Model.Ingredients[i].RecipeIngredients.FirstOrDefault()?.Quantity)" placeholder="Menge" class="form-control" />
|
||||
<select name="IngredientViewModel.Ingredients[@i].RecipeIngredients[0].Unit.Id" class="form-control">
|
||||
@foreach (var unit in Model.Units)
|
||||
{
|
||||
@if (Model.Ingredients[i].RecipeIngredients.FirstOrDefault()?.Unit?.Id == unit.Id)
|
||||
{
|
||||
<option value="@unit.Id" selected>@unit.Name (@unit.Symbol)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@unit.Id">@unit.Name (@unit.Symbol)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
<button type="button" class="btn-delete" onclick="removeIngredient('@Model.Ingredients[i].Id')">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-add" onclick="addIngredient()">Zutat hinzufügen</button>
|
||||
|
||||
|
||||
@@ -63,7 +63,18 @@
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" integrity="sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
// Light/Dark mode toggle logic
|
||||
|
||||
let recipeId;
|
||||
@if (ViewData["RecipeId"] != null)
|
||||
{
|
||||
<text>recipeId = '@ViewData["RecipeId"]';</text>
|
||||
}
|
||||
else if (ViewData["CategoryId"] != null)
|
||||
{
|
||||
<text>recipeId = '@ViewData["CategoryId"]';</text>
|
||||
}
|
||||
|
||||
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
const lightModeToggle = document.getElementById('lightModeToggle');
|
||||
darkModeToggle.addEventListener('click', () => {
|
||||
@@ -72,8 +83,8 @@
|
||||
lightModeToggle.addEventListener('click', () => {
|
||||
document.body.classList.remove('bg-dark', 'text-white');
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
|
||||
|
||||
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
|
||||
if (token) {
|
||||
@@ -81,7 +92,7 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,3 +20,68 @@ 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;
|
||||
}
|
||||
@@ -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 = `
|
||||
<div class="instruction-item" id="instruction-new-${index}">
|
||||
<div class="instruction-controls">
|
||||
<input type="file" name="InstructionViewModel.Instructions[${index}].MediaFile" class="instruction-file" />
|
||||
<textarea name="InstructionViewModel.Instructions[${index}].Description" placeholder="Beschreibung" class="form-control"></textarea>
|
||||
<button type="button" class="btn-delete" onclick="document.getElementById('instruction-new-${index}').remove()">🗑️</button>
|
||||
</div>
|
||||
<div class="instruction-actions">
|
||||
<button type="button" class="btn-move-up" disabled>⬆️</button>
|
||||
<button type="button" class="btn-move-down" disabled>⬇️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="ingredient-item" id="ingredient-new-${index}">
|
||||
<div class="ingredient-controls">
|
||||
<input type="text" name="IngredientViewModel.Ingredients[${index}].Name" placeholder="Name" class="form-control" />
|
||||
<input type="number" name="IngredientViewModel.Ingredients[${index}].RecipeIngredients[0].Quantity" placeholder="Menge" class="form-control" />
|
||||
<select name="IngredientViewModel.Ingredients[${index}].RecipeIngredients[0].Unit.Id" class="form-control unit-select">
|
||||
<option value="">Lade Einheiten...</option>
|
||||
</select>
|
||||
<button type="button" class="btn-delete" onclick="document.getElementById('ingredient-new-${index}').remove()">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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 = '<option value="">Fehler beim Laden</option>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching units:', error);
|
||||
unitSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Write your JavaScript code.
|
||||
|
||||
Reference in New Issue
Block a user