diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index e19eea3..46177ee 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -305,8 +305,8 @@ return View(); } - // POST: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} - [HttpPost("{recipeId}/RemoveIngredient/{ingredientId}")] + // DELETE: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} + [HttpDelete("{recipeId}/RemoveIngredient/{ingredientId}")] [ValidateAntiForgeryToken] public async Task RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId) { @@ -338,8 +338,8 @@ return View(); } - // POST: /Recipe/{recipeId}/RemoveInstruction/{instructionId} - [HttpPost("{recipeId}/RemoveInstruction/{instructionId}")] + // DELETE: /Recipe/{recipeId}/RemoveInstruction/{instructionId} + [HttpDelete("{recipeId}/RemoveInstruction/{instructionId}")] [ValidateAntiForgeryToken] public async Task RemoveInstructionConfirmed(Guid recipeId, Guid instructionId) { @@ -412,7 +412,7 @@ } catch (Exception ex) { - return BadRequest(ex.Message); + return BadRequest(ex.Message); } } @@ -470,5 +470,50 @@ return PartialView("_IngredientsPartial", viewModel); } + + // GET: /Recipe/{recipeId}/AdjustableIngredients + [HttpGet("{recipeId}/AdjustableIngredients")] + public async Task GetAdjustableIngredients(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return PartialView("_AdjustableIngredientsPartial", recipe); + } + + // GET: /Recipe/{recipeId}/Delete + [HttpGet("{recipeId}/Delete")] + public async Task Delete(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return View(recipe); + } + + // DELETE: /Recipe/{recipeId}/Delete + [HttpDelete("{recipeId}/Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(Guid recipeId) + { + var deleted = await _recipeRepository.DeleteRecipeAsync(recipeId); + + if (deleted) + { + TempData["SuccessMessage"] = "Rezept wurde erfolgreich gelöscht."; + return RedirectToAction("Index", "Home"); + } + else + { + TempData["ErrorMessage"] = "Rezept nicht gefunden oder konnte nicht gelöscht werden."; + return RedirectToAction("Details", new { recipeId }); + } + } } } diff --git a/Francesco.Recipes.World/Francesco.Recipes.World.csproj b/Francesco.Recipes.World/Francesco.Recipes.World.csproj index 84f6487..e28537c 100644 --- a/Francesco.Recipes.World/Francesco.Recipes.World.csproj +++ b/Francesco.Recipes.World/Francesco.Recipes.World.csproj @@ -49,7 +49,6 @@ - diff --git a/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs index 6807402..e98a564 100644 --- a/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs +++ b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs @@ -1,11 +1,8 @@ // -using System; using Francesco.Recipes.World.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index fdbc876..5afb2f0 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -18,5 +18,7 @@ Task CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime); Task> SearchInRecipesAndIngredients(string searchterm); + + Task DeleteRecipeAsync(Guid recipeId); } } diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index 382bf90..73c76a7 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -38,6 +38,7 @@ .ThenInclude(ri => ri.Unit) .Include(r => r.MediaFiles) .Include(r => r.Instructions) + .ThenInclude(i => i.MediaFiles) .FirstOrDefaultAsync(r => r.Id == recipeId); } @@ -182,5 +183,48 @@ .ThenInclude(ri => ri.Ingredient) .ToListAsync(); } + + public async Task DeleteRecipeAsync(Guid recipeId) + { + var recipe = await GetRecipeByIdAsync(recipeId); + + if (recipe == null) + { + return false; + } + + if (recipe.RecipeIngredients != null && recipe.RecipeIngredients.Any()) + { + _context.RecipeIngredients.RemoveRange(recipe.RecipeIngredients); + } + + if (recipe.Instructions != null && recipe.Instructions.Any()) + { + foreach (var instruction in recipe.Instructions) + { + if (instruction.MediaFiles != null && instruction.MediaFiles.Any()) + { + _context.MediaFiles.RemoveRange(instruction.MediaFiles); + } + } + + _context.Instructions.RemoveRange(recipe.Instructions); + } + + if (recipe.MediaFiles != null && recipe.MediaFiles.Any()) + { + _context.MediaFiles.RemoveRange(recipe.MediaFiles); + } + + if (recipe.Favorit != null && recipe.Favorit.Id != Guid.Empty) + { + _context.Remove(recipe.Favorit); + } + + _context.Recipes.Remove(recipe); + + await _context.SaveChangesAsync(); + return true; + } } } diff --git a/Francesco.Recipes.World/Views/Home/Index.cshtml b/Francesco.Recipes.World/Views/Home/Index.cshtml index af2e530..0c9e60c 100644 --- a/Francesco.Recipes.World/Views/Home/Index.cshtml +++ b/Francesco.Recipes.World/Views/Home/Index.cshtml @@ -60,7 +60,7 @@ @await Html.PartialAsync("_FavoriteButton", recipe) - Details diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml index 4ea25c7..545b9f3 100644 --- a/Francesco.Recipes.World/Views/Recipe/Details.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Details.cshtml @@ -6,7 +6,7 @@

@Model.Name

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

Description: @Model.Description

Difficulty: @Model.Difficulty

-

Servings: @Model.Servings

Preparation Time: @Model.PreparationTime

Cooking Time: @Model.CookingTime

-
-

Ingredients

-
-
    - @foreach (var ingredient in Model.RecipeIngredients) - { -
  • - - @ingredient.Ingredient.Name - @ingredient.Quantity @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty) -
  • - } -
- -
-
+ + @await Html.PartialAsync("_AdjustableIngredientsPartial", Model) + + @await Html.PartialAsync("_RecipeInstructionGridPartial", new Francesco.Recipes.World.Models.InstructionViewModel +{ + RecipeId = Model.Id, + Instructions = Model.Instructions.ToList() +}) + +
+ @Html.AntiForgeryToken() + +
@section Scripts { } diff --git a/Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml new file mode 100644 index 0000000..7855058 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml @@ -0,0 +1,36 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe + +
+
+

Ingredients

+
+ +
+ + + +
+ (Original: @Model.Servings) +
+
+ +
+
    + @foreach (var ingredient in Model.RecipeIngredients) + { +
  • + + @ingredient.Ingredient.Name - + @ingredient.Quantity + @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty) + +
  • + } +
+ +
+
diff --git a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml index a06f9bd..55fbcc0 100644 --- a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml @@ -7,6 +7,7 @@ +
diff --git a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml new file mode 100644 index 0000000..c9b7df8 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml @@ -0,0 +1,31 @@ +@model Francesco.Recipes.World.Models.InstructionViewModel + +
+

Instructions

+ +
+ @foreach (var instruction in Model.Instructions.OrderBy(i => i.Number)) + { +
+
+ @if (instruction.MediaFiles != null && instruction.MediaFiles.Any()) + { + var mediaFile = instruction.MediaFiles.First(); + if (mediaFile.Data != null && mediaFile.Data.Length > 0) + { + Step @instruction.Number + } + } + else + { +
+ +
+ } + @instruction.Number +
+

@instruction.Description

+
+ } +
+
\ No newline at end of file diff --git a/Francesco.Recipes.World/wwwroot/css/site.css b/Francesco.Recipes.World/wwwroot/css/site.css index a505610..c895bb3 100644 --- a/Francesco.Recipes.World/wwwroot/css/site.css +++ b/Francesco.Recipes.World/wwwroot/css/site.css @@ -84,4 +84,103 @@ textarea.form-control { border-radius: 4px; cursor: pointer; margin-top: 10px; -} \ No newline at end of file +} + + +/* Recipe Instructions Grid Layout */ +.instructions-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-top: 2rem; + margin-bottom: 2rem; +} + +@media (max-width: 992px) { + .instructions-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 576px) { + .instructions-grid { + grid-template-columns: 1fr; + } +} + +.instruction-card { + border: 1px solid #ccc; + padding: 1rem; + border-radius: 8px; + background-color: white; +} + +.instruction-image { + position: relative; + width: 100%; + aspect-ratio: 1 / 1; + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + background-color: #f8f9fa; + text-align: center; +} + + .instruction-image img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 4px; + } + +.placeholder-image { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + color: #999; + background-color: #f0f0f0; + border-radius: 4px; +} + +.step-number { + position: absolute; + bottom: 8px; + right: 8px; + background: rgba(0, 0, 0, 0.6); + color: white; + padding: 3px 8px; + border-radius: 50%; + font-size: 0.875rem; + font-weight: bold; +} + +.instruction-text { + font-size: 0.9rem; + line-height: 1.4; + color: #333; +} + +.recipe-instructions-section h3 { + margin-bottom: 1.5rem; + position: relative; + padding-bottom: 0.5rem; +} + + .recipe-instructions-section h3:after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 50px; + height: 2px; + background-color: #007bff; + } + + diff --git a/Francesco.Recipes.World/wwwroot/js/site.js b/Francesco.Recipes.World/wwwroot/js/site.js index 12bdab8..20666ef 100644 --- a/Francesco.Recipes.World/wwwroot/js/site.js +++ b/Francesco.Recipes.World/wwwroot/js/site.js @@ -189,3 +189,25 @@ async function addIngredient() { } } + + async function addSelectedIngredientsToShoppingList() { + var form = document.getElementById('ingredient-form'); + var formData = new FormData(form); + var selectedIngredientIds = formData.getAll('ingredientIds'); + + var response = await fetch('/ShoppingList/CreateOrAddIngredients', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ recipeId: '@Model.Id', ingredientIds: selectedIngredientIds }) + }); + + if (response.ok) { + var result = await response.json(); + localStorage.setItem('shoppingListId', result.shoppingListId); + alert('Shopping list updated.'); + } else { + alert('Failed to update shopping list.'); + } +} \ No newline at end of file