Merge develop into feature/Show-ShoppingList-Of-Active-RecipeCard
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Francesco.Recipes.World.Constants
|
||||||
|
{
|
||||||
|
public class ContentType
|
||||||
|
{
|
||||||
|
public const string Image = "image/";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Francesco.Recipes.World.Constants
|
||||||
|
{
|
||||||
|
public class SortOrders
|
||||||
|
{
|
||||||
|
public const string Newest = "newest";
|
||||||
|
public const string Oldest = "oldest";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
namespace Francesco.Recipes.World.Controller.Favorite
|
||||||
|
{
|
||||||
|
using Francesco.Recipes.World.Constants;
|
||||||
|
using Francesco.Recipes.World.Models;
|
||||||
|
using Francesco.Recipes.World.Repositories.Favorit;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
[Route("Favorite")]
|
||||||
|
public class FavoriteController : Controller
|
||||||
|
{
|
||||||
|
private readonly IFavoriteRepository _favoriteRepository;
|
||||||
|
|
||||||
|
public FavoriteController(IFavoriteRepository favoriteRepository)
|
||||||
|
{
|
||||||
|
_favoriteRepository = favoriteRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Index(string sortOrder = SortOrders.Newest)
|
||||||
|
{
|
||||||
|
var favoriteRecipes = await _favoriteRepository.GetFavoriteRecipesAsync();
|
||||||
|
|
||||||
|
var sortedRecipes = sortOrder == SortOrders.Oldest
|
||||||
|
? favoriteRecipes.OrderBy(r => r.Favorite.CreatedAt)
|
||||||
|
: favoriteRecipes.OrderByDescending(r => r.Favorite.CreatedAt);
|
||||||
|
|
||||||
|
var viewModel = new FavoriteViewModel
|
||||||
|
{
|
||||||
|
FavoriteRecipes = sortedRecipes,
|
||||||
|
SortOrder = sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return BadRequest(ex.Message);
|
return BadRequest(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,5 +470,50 @@
|
|||||||
|
|
||||||
return PartialView("_IngredientsPartial", viewModel);
|
return PartialView("_IngredientsPartial", viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /Recipe/{recipeId}/AdjustableIngredients
|
||||||
|
[HttpGet("{recipeId}/AdjustableIngredients")]
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,6 @@
|
|||||||
<Folder Include="Services\Category\" />
|
<Folder Include="Services\Category\" />
|
||||||
<Folder Include="Services\MediaFile\" />
|
<Folder Include="Services\MediaFile\" />
|
||||||
<Folder Include="Services\Ingredient\" />
|
<Folder Include="Services\Ingredient\" />
|
||||||
<Folder Include="Services\Recipe\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+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("20250602133408_RenameFavoriteColumn")]
|
||||||
|
partial class RenameFavoriteColumn
|
||||||
|
{
|
||||||
|
/// <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>("FavoriteId")
|
||||||
|
.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("FavoriteId");
|
||||||
|
|
||||||
|
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", "Favorite")
|
||||||
|
.WithMany("Recipe")
|
||||||
|
.HasForeignKey("FavoriteId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Category");
|
||||||
|
|
||||||
|
b.Navigation("Favorite");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient")
|
||||||
|
.WithMany("RecipeIngredients")
|
||||||
|
.HasForeignKey("IngredientId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
|
||||||
|
.WithMany("RecipeIngredients")
|
||||||
|
.HasForeignKey("RecipeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit")
|
||||||
|
.WithMany("RecipeIngredient")
|
||||||
|
.HasForeignKey("UnitId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Ingredient");
|
||||||
|
|
||||||
|
b.Navigation("Recipe");
|
||||||
|
|
||||||
|
b.Navigation("Unit");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RecipeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList")
|
||||||
|
.WithMany("RecipeShoppingList")
|
||||||
|
.HasForeignKey("ShoppingListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Recipe");
|
||||||
|
|
||||||
|
b.Navigation("ShoppingList");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Recipes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Recipe");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("IngredientShoppingLists");
|
||||||
|
|
||||||
|
b.Navigation("RecipeIngredients");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("MediaFiles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Instructions");
|
||||||
|
|
||||||
|
b.Navigation("MediaFiles");
|
||||||
|
|
||||||
|
b.Navigation("RecipeIngredients");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("SelectedIngredients");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RecipeShoppingList");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RecipeIngredient");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Francesco.Recipes.World.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RenameFavoriteColumn : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
if (migrationBuilder is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Recipes_Favorits_FavoritId",
|
||||||
|
table: "Recipes");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "FavoritId",
|
||||||
|
table: "Recipes",
|
||||||
|
newName: "FavoriteId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Recipes_FavoritId",
|
||||||
|
table: "Recipes",
|
||||||
|
newName: "IX_Recipes_FavoriteId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Recipes_Favorits_FavoriteId",
|
||||||
|
table: "Recipes",
|
||||||
|
column: "FavoriteId",
|
||||||
|
principalTable: "Favorits",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
if (migrationBuilder is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(migrationBuilder));
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Recipes_Favorits_FavoriteId",
|
||||||
|
table: "Recipes");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "FavoriteId",
|
||||||
|
table: "Recipes",
|
||||||
|
newName: "FavoritId");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_Recipes_FavoriteId",
|
||||||
|
table: "Recipes",
|
||||||
|
newName: "IX_Recipes_FavoritId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Recipes_Favorits_FavoritId",
|
||||||
|
table: "Recipes",
|
||||||
|
column: "FavoritId",
|
||||||
|
principalTable: "Favorits",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -221,7 +221,7 @@ namespace Francesco.Recipes.World.Migrations
|
|||||||
b.Property<int>("Difficulty")
|
b.Property<int>("Difficulty")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<Guid>("FavoritId")
|
b.Property<Guid>("FavoriteId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<bool>("IsFavorite")
|
b.Property<bool>("IsFavorite")
|
||||||
@@ -244,7 +244,7 @@ namespace Francesco.Recipes.World.Migrations
|
|||||||
|
|
||||||
b.HasIndex("CategoryId");
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
b.HasIndex("FavoritId");
|
b.HasIndex("FavoriteId");
|
||||||
|
|
||||||
b.ToTable("Recipes");
|
b.ToTable("Recipes");
|
||||||
});
|
});
|
||||||
@@ -460,15 +460,15 @@ namespace Francesco.Recipes.World.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit")
|
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorite")
|
||||||
.WithMany("Recipe")
|
.WithMany("Recipe")
|
||||||
.HasForeignKey("FavoritId")
|
.HasForeignKey("FavoriteId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("Category");
|
b.Navigation("Category");
|
||||||
|
|
||||||
b.Navigation("Favorit");
|
b.Navigation("Favorite");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
|
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class Recipe : ITimeStampedEntity
|
|||||||
|
|
||||||
public virtual ICollection<MediaFile> MediaFiles { get; set; } = new List<MediaFile>();
|
public virtual ICollection<MediaFile> MediaFiles { get; set; } = new List<MediaFile>();
|
||||||
|
|
||||||
public virtual Favorit Favorit { get; set; } = new ();
|
public virtual Favorit Favorite { get; set; } = new ();
|
||||||
|
|
||||||
public virtual Category Category { get; set; } = new ();
|
public virtual Category Category { get; set; } = new ();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Francesco.Recipes.World.Constants;
|
||||||
|
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
||||||
|
|
||||||
|
namespace Francesco.Recipes.World.Models
|
||||||
|
{
|
||||||
|
public class FavoriteViewModel
|
||||||
|
{
|
||||||
|
public IEnumerable<Recipe> FavoriteRecipes { get; set; } = new List<Recipe>();
|
||||||
|
|
||||||
|
public string SortOrder { get; set; } = SortOrders.Newest;
|
||||||
|
|
||||||
|
public bool HasFavorites => FavoriteRecipes.Any();
|
||||||
|
|
||||||
|
public string SortOrderDisplayText => SortOrder == SortOrders.Oldest ? "Älteste Favorits" : "Neueste Favorits";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Francesco.Recipes.World.Models
|
||||||
|
{
|
||||||
|
public class SearchViewModel
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public bool IsFavorite { get; set; }
|
||||||
|
|
||||||
|
public byte[]? ImageData { get; set; }
|
||||||
|
|
||||||
|
public string? MimeType { get; set; }
|
||||||
|
|
||||||
|
public List<string> Ingredients { get; set; } = new ();
|
||||||
|
|
||||||
|
public TimeSpan TotalTime { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,8 @@ var connectionString = builder.Configuration.GetConnectionString("FrancescosReci
|
|||||||
?? throw new InvalidOperationException("Connection string 'FrancescosRecipesWorldDbContextConnection' not found.");
|
?? throw new InvalidOperationException("Connection string 'FrancescosRecipesWorldDbContextConnection' not found.");
|
||||||
|
|
||||||
services.AddDbContext<FrancescosRecipesWorldDbContext>(options =>
|
services.AddDbContext<FrancescosRecipesWorldDbContext>(options =>
|
||||||
options.UseSqlServer(connectionString));
|
options.UseSqlServer(connectionString, sqlOptions =>
|
||||||
|
sqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)));
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
return await _context.Categories
|
return await _context.Categories
|
||||||
.Include(c => c.Recipes)
|
.Include(c => c.Recipes)
|
||||||
.ThenInclude(r => r.MediaFiles)
|
.ThenInclude(r => r.MediaFiles)
|
||||||
|
.AsSplitQuery()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@
|
|||||||
public async Task<IEnumerable<Recipe>> GetFavoriteRecipesAsync()
|
public async Task<IEnumerable<Recipe>> GetFavoriteRecipesAsync()
|
||||||
{
|
{
|
||||||
return await _context.Recipes
|
return await _context.Recipes
|
||||||
.Where(r => r.IsFavorite)
|
.Where(r => r.IsFavorite)
|
||||||
.ToListAsync();
|
.Include(r => r.Favorite)
|
||||||
|
.Include(r => r.MediaFiles)
|
||||||
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsFavoriteAsync(Guid recipeId)
|
public async Task<bool> IsFavoriteAsync(Guid recipeId)
|
||||||
@@ -28,12 +30,32 @@
|
|||||||
|
|
||||||
public async Task AddFavoriteAsync(Guid recipeId)
|
public async Task AddFavoriteAsync(Guid recipeId)
|
||||||
{
|
{
|
||||||
var recipe = await _context.Recipes.FindAsync(recipeId);
|
var recipe = await _context.Recipes
|
||||||
if (recipe != null && !recipe.IsFavorite)
|
.Include(r => r.Favorite)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == recipeId);
|
||||||
|
|
||||||
|
if (recipe == null)
|
||||||
{
|
{
|
||||||
recipe.IsFavorite = true;
|
throw new InvalidOperationException("Recipe not found.");
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipe.IsFavorite)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Recipe is already a favorite.");
|
||||||
|
}
|
||||||
|
|
||||||
|
recipe.IsFavorite = true;
|
||||||
|
|
||||||
|
if (recipe.Favorite == null || recipe.Favorite.Id == Guid.Empty)
|
||||||
|
{
|
||||||
|
recipe.Favorite = new Models.BackendModels.Favorit.Favorit
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveFavoriteAsync(Guid recipeId)
|
public async Task RemoveFavoriteAsync(Guid recipeId)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
namespace Francesco.Recipes.World.Repositories.MediaFile
|
namespace Francesco.Recipes.World.Repositories.MediaFile
|
||||||
{
|
{
|
||||||
|
using Francesco.Recipes.World.Constants;
|
||||||
using Francesco.Recipes.World.Data;
|
using Francesco.Recipes.World.Data;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.MediaFile;
|
using Francesco.Recipes.World.Models.BackendModels.MediaFile;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
||||||
@@ -83,7 +84,7 @@
|
|||||||
{
|
{
|
||||||
var recipe = await _recipeRepository.GetRecipeAsync(recipeId);
|
var recipe = await _recipeRepository.GetRecipeAsync(recipeId);
|
||||||
|
|
||||||
var isImage = mediaFile.ContentType.StartsWith("image/");
|
var isImage = mediaFile.ContentType.StartsWith(ContentType.Image);
|
||||||
var isVideo = mediaFile.ContentType.StartsWith("video/");
|
var isVideo = mediaFile.ContentType.StartsWith("video/");
|
||||||
|
|
||||||
if (!isImage && !isVideo)
|
if (!isImage && !isVideo)
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
|
|
||||||
if (isImage)
|
if (isImage)
|
||||||
{
|
{
|
||||||
await RemoveExistingMediaAsync(recipe, "image/");
|
await RemoveExistingMediaAsync(recipe, ContentType.Image);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
namespace Francesco.Recipes.World.Repositories.Recipe
|
namespace Francesco.Recipes.World.Repositories.Recipe
|
||||||
{
|
{
|
||||||
|
using Francesco.Recipes.World.Models;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Category;
|
using Francesco.Recipes.World.Models.BackendModels.Category;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
|
|
||||||
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
|
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
|
||||||
|
|
||||||
Task<IEnumerable<Recipe>> SearchInRecipesAndIngredients(string searchterm);
|
Task<bool> DeleteRecipeAsync(Guid recipeId);
|
||||||
|
Task<IEnumerable<SearchViewModel>> SearchInRecipesAndIngredients(string searchTerm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace Francesco.Recipes.World.Repositories.Recipe
|
namespace Francesco.Recipes.World.Repositories.Recipe
|
||||||
{
|
{
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using Francesco.Recipes.World.Constants;
|
||||||
using Francesco.Recipes.World.Data;
|
using Francesco.Recipes.World.Data;
|
||||||
|
using Francesco.Recipes.World.Models;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Category;
|
using Francesco.Recipes.World.Models.BackendModels.Category;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Ingredient;
|
using Francesco.Recipes.World.Models.BackendModels.Ingredient;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
.ThenInclude(ri => ri.Unit)
|
.ThenInclude(ri => ri.Unit)
|
||||||
.Include(r => r.MediaFiles)
|
.Include(r => r.MediaFiles)
|
||||||
.Include(r => r.Instructions)
|
.Include(r => r.Instructions)
|
||||||
|
.ThenInclude(i => i.MediaFiles)
|
||||||
.FirstOrDefaultAsync(r => r.Id == recipeId);
|
.FirstOrDefaultAsync(r => r.Id == recipeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,21 +151,32 @@
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Recipe>> SearchInRecipesAndIngredients(string searchTerm)
|
public async Task<IEnumerable<SearchViewModel>> SearchInRecipesAndIngredients(string searchTerm)
|
||||||
{
|
{
|
||||||
var queryable = _context.Recipes
|
try
|
||||||
.Include(r => r.RecipeIngredients)
|
|
||||||
.ThenInclude(ri => ri.Ingredient)
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
|
||||||
{
|
{
|
||||||
searchTerm = searchTerm.ToLower();
|
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||||
queryable = queryable.Where(r => r.Name.ToLower().Contains(searchTerm) ||
|
{
|
||||||
r.RecipeIngredients.Any(ri => ri.Ingredient.Name.ToLower().Contains(searchTerm)));
|
return await _context.Recipes
|
||||||
}
|
.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.Take(20)
|
||||||
|
.Select(SearchViewModelSelector())
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
return await queryable.ToListAsync();
|
var normalizedSearchTerm = searchTerm.ToLower();
|
||||||
|
|
||||||
|
return await ApplyRecipeSearchFilter(_context.Recipes, normalizedSearchTerm)
|
||||||
|
.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.Take(100)
|
||||||
|
.Select(SearchViewModelSelector())
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error in SearchInRecipesAndIngredientsOptimized: {ex.Message}");
|
||||||
|
return new List<SearchViewModel>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyCollection<Recipe>> GetRecipesByDifficultyAsync(Difficulty? difficulty)
|
public async Task<IReadOnlyCollection<Recipe>> GetRecipesByDifficultyAsync(Difficulty? difficulty)
|
||||||
@@ -182,5 +197,77 @@
|
|||||||
.ThenInclude(ri => ri.Ingredient)
|
.ThenInclude(ri => ri.Ingredient)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<Recipe, SearchViewModel>> SearchViewModelSelector()
|
||||||
|
{
|
||||||
|
return r => new SearchViewModel
|
||||||
|
{
|
||||||
|
Id = r.Id,
|
||||||
|
Name = r.Name,
|
||||||
|
Description = r.Description,
|
||||||
|
IsFavorite = r.IsFavorite,
|
||||||
|
ImageData = r.MediaFiles
|
||||||
|
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
|
||||||
|
.Select(m => m.Data)
|
||||||
|
.FirstOrDefault(),
|
||||||
|
MimeType = r.MediaFiles
|
||||||
|
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
|
||||||
|
.Select(m => m.MimeType)
|
||||||
|
.FirstOrDefault(),
|
||||||
|
Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(),
|
||||||
|
TotalTime = r.PreparationTime.Add(r.CookingTime),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<Recipe> ApplyRecipeSearchFilter(IQueryable<Recipe> query, string normalizedSearchTerm)
|
||||||
|
{
|
||||||
|
return query.Where(r =>
|
||||||
|
EF.Functions.Like(r.Name.ToLower(), $"%{normalizedSearchTerm}%") ||
|
||||||
|
(r.Description != null && EF.Functions.Like(r.Description.ToLower(), $"%{normalizedSearchTerm}%")) ||
|
||||||
|
r.RecipeIngredients.Any(ri => EF.Functions.Like(ri.Ingredient.Name.ToLower(), $"%{normalizedSearchTerm}%")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@model Francesco.Recipes.World.Models.FavoriteViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Favoriten";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2 class="mb-4">Favoriten</h2>
|
||||||
|
|
||||||
|
<div class="mb-4 text-end">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="sortDropdown"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
@Model.SortOrderDisplayText
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="sortDropdown">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item @(Model.SortOrder == "newest" ? "active" : "")"
|
||||||
|
href="@Url.Action("Index", new { sortOrder = "newest" })">Neueste Favorits</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item @(Model.SortOrder == "oldest" ? "active" : "")"
|
||||||
|
href="@Url.Action("Index", new { sortOrder = "oldest" })">Älteste Favorits</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!Model.HasFavorites)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Keine Favoriten vorhanden. Füge Rezepte zu deinen Favoriten hinzu, indem du auf den Stern klickst.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row row-cols-1 row-cols-md-3 g-4">
|
||||||
|
@foreach (var recipe in Model.FavoriteRecipes)
|
||||||
|
{
|
||||||
|
<div class="col">
|
||||||
|
<div class="card h-100">
|
||||||
|
@{
|
||||||
|
var mediaFile = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType != null && m.MimeType.StartsWith("image/"));
|
||||||
|
var imageData = mediaFile?.Data;
|
||||||
|
var mimeType = mediaFile?.MimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="card-img-top text-center bg-light" style="height:200px; display:flex; align-items:center; justify-content:center;">
|
||||||
|
@if (imageData != null && mimeType != null)
|
||||||
|
{
|
||||||
|
<img src="data:@mimeType;base64,@Convert.ToBase64String(imageData)"
|
||||||
|
alt="@recipe.Name" class="img-fluid" style="max-height:100%; max-width:100%; object-fit:contain;" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-secondary">Kein Bild</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">@recipe.Name</h5>
|
||||||
|
<div class="d-flex align-items-center mt-3">
|
||||||
|
<span class="me-3">
|
||||||
|
@await Html.PartialAsync("_FavoriteButton", recipe)
|
||||||
|
</span>
|
||||||
|
<span class="text-muted">
|
||||||
|
<i class="bi bi-clock"></i> @(recipe.PreparationTime.TotalMinutes + recipe.CookingTime.TotalMinutes)min
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-white">
|
||||||
|
<a href="@Url.Action("Details", "Recipe", new { recipeId = recipe.Id })"
|
||||||
|
class="btn btn-outline-primary btn-sm w-100">Details</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,42 +1,42 @@
|
|||||||
@model IEnumerable<Francesco.Recipes.World.Views.Category.CategoryRecipesViewModel>
|
@model IEnumerable<Francesco.Recipes.World.Views.Category.CategoryRecipesViewModel>
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
<div class="welcome-banner text-center mb-4">
|
<div class="welcome-banner text-center mb-4">
|
||||||
<img src="/images/banner2.jpg" alt="Willkommen" class="img-fluid" />
|
<img src="/images/banner2.jpg" alt="Willkommen" class="img-fluid" />
|
||||||
<h1 class="mt-3">Willkommen in der Rezept-App</h1>
|
<h1 class="mt-3">Willkommen in der Rezept-App</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form hx-get="/Home/Search"
|
<form hx-get="/Home/Search"
|
||||||
hx-target="#search-results"
|
hx-target="#search-results"
|
||||||
hx-trigger="keyup changed delay:300ms"
|
hx-trigger="keyup changed delay:300ms"
|
||||||
onsubmit="return false;"
|
onsubmit="return false;"
|
||||||
class="mb-4">
|
class="mb-4">
|
||||||
<input type="text" name="query" class="form-control" placeholder="Suchen nach Rezepten oder Zutaten..." autocomplete="off" />
|
<input type="text" name="term" class="form-control" placeholder="Suchen nach Rezepten oder Zutaten..." autocomplete="off" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<div id="search-results" class="mt-4"></div>
|
<div id="search-results" class="mt-4"></div>
|
||||||
|
|
||||||
|
|
||||||
@foreach (var category in Model)
|
@foreach (var category in Model)
|
||||||
{
|
{
|
||||||
<div class="category-section mb-5" id="category-@category.Category.Id">
|
<div class="category-section mb-5" id="category-@category.Category.Id">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h2>@category.Category.Name</h2>
|
<h2>@category.Category.Name</h2>
|
||||||
<a href="/Category/Details/@category.Category.Id"
|
<a href="/Category/Details/@category.Category.Id"
|
||||||
class="btn btn-link"
|
class="btn btn-link"
|
||||||
hx-get="/Category/Details/@category.Category.Id"
|
hx-get="/Category/Details/@category.Category.Id"
|
||||||
hx-target="#category-@category.Category.Id"
|
hx-target="#category-@category.Category.Id"
|
||||||
hx-swap="outerHTML">Alle @category.Category.Name-Rezepte anzeigen</a>
|
hx-swap="outerHTML">Alle @category.Category.Name-Rezepte anzeigen</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@foreach (var recipe in category.Recipes)
|
@foreach (var recipe in category.Recipes)
|
||||||
{
|
{
|
||||||
var mediaFile = recipe.MediaFiles?.FirstOrDefault();
|
var mediaFile = recipe.MediaFiles?.FirstOrDefault();
|
||||||
var imageData = mediaFile?.Data;
|
var imageData = mediaFile?.Data;
|
||||||
var mimeType = mediaFile?.MimeType;
|
var mimeType = mediaFile?.MimeType;
|
||||||
|
|
||||||
<div class="col-md-3 mb-4">
|
<div class="col-md-3 mb-4">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
@@ -66,16 +66,14 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="col-md-3 mb-4">
|
<div class="col-md-3 mb-4">
|
||||||
<div class="card h-100 text-center">
|
<div class="card h-100 text-center">
|
||||||
<div class="card-body d-flex flex-column justify-content-center">
|
<div class="card-body d-flex flex-column justify-content-center">
|
||||||
<a href="/Create/@category.Category.Id"
|
<a href="/Create/@category.Category.Id"
|
||||||
class="btn btn-outline-primary">Rezept hinzufügen</a>
|
class="btn btn-outline-primary">Rezept hinzufügen</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +1,75 @@
|
|||||||
@model IEnumerable<Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe>
|
@model IEnumerable<Francesco.Recipes.World.Models.SearchViewModel>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
if (!Model.Any())
|
if (!Model.Any())
|
||||||
{
|
{
|
||||||
<p class="text-muted">Keine Ergebnisse gefunden.</p>
|
<p class="text-muted">Keine Ergebnisse gefunden.</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@foreach (var recipe in Model)
|
@foreach (var recipe in Model)
|
||||||
{
|
{
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<div class="card h-100 shadow-sm">
|
<div class="card h-100 shadow-sm">
|
||||||
<div class="recipe-image">
|
<div class="recipe-image">
|
||||||
@{
|
@if (recipe.ImageData != null && recipe.MimeType != null)
|
||||||
var mediaFile = recipe.MediaFiles.FirstOrDefault();
|
{
|
||||||
|
<img src="data:@recipe.MimeType;base64,@Convert.ToBase64String(recipe.ImageData)"
|
||||||
if (mediaFile?.Data != null)
|
alt="@recipe.Name"
|
||||||
{
|
class="card-img-top" />
|
||||||
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)"
|
}
|
||||||
alt="@recipe.Name"
|
else
|
||||||
class="card-img-top" />
|
{
|
||||||
}
|
<img src="~/images/placeholder.png"
|
||||||
else
|
alt="Platzhalter"
|
||||||
{
|
class="card-img-top" />
|
||||||
<img src="~/images/placeholder.png"
|
}
|
||||||
alt="Platzhalter"
|
|
||||||
class="card-img-top" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body d-flex flex-column justify-content-between">
|
|
||||||
<div>
|
|
||||||
<h5 class="card-title">@recipe.Name</h5>
|
|
||||||
<p class="card-text text-muted mb-2">
|
|
||||||
<i class="bi bi-clock"></i>
|
|
||||||
@recipe.PreparationTime.Hours h @recipe.PreparationTime.Minutes min
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="card-body d-flex flex-column justify-content-between">
|
||||||
<button class="btn btn-outline-secondary"
|
<div>
|
||||||
hx-post="/Recipe/ToggleFavorite"
|
<h5 class="card-title">@recipe.Name</h5>
|
||||||
hx-vals='{"id": "@recipe.Id"}'
|
<p class="card-text text-muted mb-2">
|
||||||
hx-swap="outerHTML">
|
<i class="bi bi-clock"></i>
|
||||||
<i class="bi @((recipe.IsFavorite) ? "bi-star-fill" : "bi-star")"></i>
|
@recipe.TotalTime.Hours h @recipe.TotalTime.Minutes min
|
||||||
</button>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<div class="favorite-button-container">
|
||||||
|
<form hx-post="@Url.Action(recipe.IsFavorite ? "RemoveFavorite" : "AddFavorite", "Recipe")"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="recipeId" value="@recipe.Id" />
|
||||||
|
<button type="submit" class="btn btn-link p-0" style="width: 40px; height: 40px;">
|
||||||
|
@if (recipe.IsFavorite)
|
||||||
|
{
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFD700">
|
||||||
|
<path d="M12 17.27L18.18 21 16.54 13.97
|
||||||
|
22 9.24l-7.19-.62L12 2 9.19 8.62
|
||||||
|
2 9.24l5.46 4.73L5.82 21z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 17.27L18.18 21 16.54 13.97
|
||||||
|
22 9.24l-7.19-.62L12 2 9.19 8.62
|
||||||
|
2 9.24l5.46 4.73L5.82 21z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/Recipe/Details/@recipe.Id" class="btn btn-primary btn-sm">Details</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<h1>@Model.Name</h1>
|
<h1>@Model.Name</h1>
|
||||||
|
|
||||||
<div class="recipe-details">
|
<div class="recipe-details" data-recipe-id="@Model.Id">
|
||||||
<div class="recipe-image">
|
<div class="recipe-image">
|
||||||
@if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null)
|
@if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null)
|
||||||
{
|
{
|
||||||
@@ -20,34 +20,166 @@
|
|||||||
<div class="recipe-info">
|
<div class="recipe-info">
|
||||||
<p><strong>Description:</strong> @Model.Description</p>
|
<p><strong>Description:</strong> @Model.Description</p>
|
||||||
<p><strong>Difficulty:</strong> @Model.Difficulty</p>
|
<p><strong>Difficulty:</strong> @Model.Difficulty</p>
|
||||||
<p><strong>Servings:</strong> @Model.Servings</p>
|
|
||||||
<p><strong>Preparation Time:</strong> @Model.PreparationTime</p>
|
<p><strong>Preparation Time:</strong> @Model.PreparationTime</p>
|
||||||
<p><strong>Cooking Time:</strong> @Model.CookingTime</p>
|
<p><strong>Cooking Time:</strong> @Model.CookingTime</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="recipe-ingredients">
|
|
||||||
<h3>Ingredients</h3>
|
@await Html.PartialAsync("_AdjustableIngredientsPartial", Model)
|
||||||
<form id="ingredient-form">
|
|
||||||
@Html.AntiForgeryToken()
|
@await Html.PartialAsync("_RecipeInstructionGridPartial", new Francesco.Recipes.World.Models.InstructionViewModel
|
||||||
<ul id="ingredient-list">
|
{
|
||||||
@foreach (var ingredient in Model.RecipeIngredients)
|
RecipeId = Model.Id,
|
||||||
{
|
Instructions = Model.Instructions.ToList()
|
||||||
<li id="ingredient-@ingredient.Id">
|
})
|
||||||
<input type="checkbox" name="ingredientIds" value="@ingredient.Id" />
|
|
||||||
@ingredient.Ingredient.Name - @ingredient.Quantity @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty)
|
<form hx-post="@($"/{Model.Id}/Delete")"
|
||||||
</li>
|
hx-confirm="Sind Sie sicher, dass Sie dieses Rezept löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
}
|
hx-redirect="/">
|
||||||
</ul>
|
@Html.AntiForgeryToken()
|
||||||
<button type="button" class="btn btn-primary" onclick="addSelectedIngredientsToShoppingList()">Add Selected to Shopping List</button>
|
<button type="submit" class="btn btn-danger">
|
||||||
</form>
|
<i class="bi bi-trash"></i> Rezept löschen
|
||||||
</div>
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
async function addSelectedIngredientsToShoppingList() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var form = document.getElementById('ingredient-form');
|
const recipeId = '@Model.Id';
|
||||||
var formData = new FormData(form);
|
window.recipeId = recipeId;
|
||||||
var selectedIngredientIds = formData.getAll('ingredientIds');
|
|
||||||
|
const originalServings = @Model.Servings;
|
||||||
|
|
||||||
|
const specialUnits = {
|
||||||
|
discreteUnits: ["stk", "zehe", "blatt", "bund", "stange"],
|
||||||
|
smallUnits: ["msp"],
|
||||||
|
spoonUnits: {
|
||||||
|
"TL": { name: "teelöffel", toMl: 5 },
|
||||||
|
"EL": { name: "esslöffel", toMl: 15 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const conversionTable = {
|
||||||
|
"knoblauch": {
|
||||||
|
unit: "zehe",
|
||||||
|
smallAmount: 0.5
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const savedServings = localStorage.getItem(`recipe_${recipeId}_servings`);
|
||||||
|
if (savedServings) {
|
||||||
|
document.getElementById('servingsInput').value = savedServings;
|
||||||
|
adjustIngredientQuantities(parseInt(savedServings));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('decreaseServings').addEventListener('click', function () {
|
||||||
|
const input = document.getElementById('servingsInput');
|
||||||
|
const currentValue = parseInt(input.value);
|
||||||
|
if (currentValue > 1) {
|
||||||
|
input.value = currentValue - 1;
|
||||||
|
adjustIngredientQuantities(currentValue - 1);
|
||||||
|
saveServingsToLocalStorage(currentValue - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('increaseServings').addEventListener('click', function () {
|
||||||
|
const input = document.getElementById('servingsInput');
|
||||||
|
const currentValue = parseInt(input.value);
|
||||||
|
input.value = currentValue + 1;
|
||||||
|
adjustIngredientQuantities(currentValue + 1);
|
||||||
|
saveServingsToLocalStorage(currentValue + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('servingsInput').addEventListener('change', function () {
|
||||||
|
const newServings = parseInt(this.value);
|
||||||
|
if (newServings < 1) {
|
||||||
|
this.value = 1;
|
||||||
|
adjustIngredientQuantities(1);
|
||||||
|
saveServingsToLocalStorage(1);
|
||||||
|
} else {
|
||||||
|
adjustIngredientQuantities(newServings);
|
||||||
|
saveServingsToLocalStorage(newServings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function adjustIngredientQuantities(newServings) {
|
||||||
|
const ingredients = document.querySelectorAll('#adjustable-ingredient-list li');
|
||||||
|
|
||||||
|
ingredients.forEach(ingredient => {
|
||||||
|
const originalQuantity = parseFloat(ingredient.getAttribute('data-original-quantity'));
|
||||||
|
const ingredientName = ingredient.getAttribute('data-ingredient-name').toLowerCase();
|
||||||
|
const unitSymbol = ingredient.getAttribute('data-unit');
|
||||||
|
const unitName = ingredient.getAttribute('data-unit-name');
|
||||||
|
|
||||||
|
let adjustedQuantity = (originalQuantity / originalServings) * newServings;
|
||||||
|
let finalQuantity = adjustedQuantity;
|
||||||
|
let note = "";
|
||||||
|
|
||||||
|
if (specialUnits.discreteUnits.includes(unitSymbol)) {
|
||||||
|
if (adjustedQuantity < 1 && adjustedQuantity > 0) {
|
||||||
|
finalQuantity = Math.ceil(adjustedQuantity);
|
||||||
|
note = " (ggf. eine kleine/halbe nehmen)";
|
||||||
|
} else {
|
||||||
|
finalQuantity = Math.round(adjustedQuantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Convert small unit "msp" (pinch) to teaspoons (TL) for a better estimate
|
||||||
|
// 4 pinches = about 1 TL → show that as a note, rounded to 1 decimal
|
||||||
|
else if (specialUnits.smallUnits.includes(unitSymbol)) {
|
||||||
|
if (adjustedQuantity > 3) {
|
||||||
|
finalQuantity = Math.round(adjustedQuantity / 4 * 10) / 10;
|
||||||
|
note = ` (ca. ${finalQuantity} TL)`;
|
||||||
|
finalQuantity = adjustedQuantity;
|
||||||
|
} else {
|
||||||
|
finalQuantity = Math.round(adjustedQuantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Object.keys(specialUnits.spoonUnits).includes(unitSymbol)) {
|
||||||
|
if (adjustedQuantity > 4 && unitSymbol === "TL") {
|
||||||
|
const esslöffel = Math.round(adjustedQuantity / 3 * 10) / 10;
|
||||||
|
note = ` (ca. ${esslöffel} EL)`;
|
||||||
|
}
|
||||||
|
finalQuantity = Math.round(adjustedQuantity * 2) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ingredientName.includes("knoblauch") && unitSymbol === "zehe" && adjustedQuantity < 1) {
|
||||||
|
finalQuantity = 1;
|
||||||
|
note = " (kleine Zehe)";
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedQuantity = formatQuantity(finalQuantity);
|
||||||
|
ingredient.querySelector('.ingredient-quantity').textContent = formattedQuantity;
|
||||||
|
|
||||||
|
const noteElement = ingredient.querySelector('.ingredient-note');
|
||||||
|
if (noteElement) {
|
||||||
|
noteElement.textContent = note;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveServingsToLocalStorage(servings) {
|
||||||
|
localStorage.setItem(`recipe_${recipeId}_servings`, servings.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatQuantity(quantity) {
|
||||||
|
if (Number.isInteger(quantity)) {
|
||||||
|
return quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantity === 0.5 || quantity === 0.25 || quantity === 0.75) {
|
||||||
|
return quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(quantity * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addSelectedIngredientsToShoppingList = function(recipeIdParam) {
|
||||||
|
const idToUse = recipeIdParam || recipeId;
|
||||||
|
|
||||||
|
var form = document.getElementById('ingredient-form');
|
||||||
|
var formData = new FormData(form);
|
||||||
|
var selectedIngredientIds = formData.getAll('ingredientIds');
|
||||||
|
|
||||||
|
|
||||||
var token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
var token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe
|
||||||
|
|
||||||
|
<div class="adjustable-ingredients">
|
||||||
|
<div class="serving-adjustment mb-3" data-original-servings="@Model.Servings">
|
||||||
|
<h3>Ingredients</h3>
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<label for="servingsInput" class="me-2">Adjust servings:</label>
|
||||||
|
<div class="input-group" style="max-width: 150px;">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="decreaseServings">-</button>
|
||||||
|
<input type="number" class="form-control text-center" id="servingsInput" value="@Model.Servings" min="1">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="increaseServings">+</button>
|
||||||
|
</div>
|
||||||
|
<span class="ms-2 text-muted">(Original: @Model.Servings)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="ingredient-form">
|
||||||
|
<ul id="adjustable-ingredient-list">
|
||||||
|
@foreach (var ingredient in Model.RecipeIngredients)
|
||||||
|
{
|
||||||
|
<li id="ingredient-@ingredient.Id"
|
||||||
|
data-ingredient-id="@ingredient.Id"
|
||||||
|
data-original-quantity="@ingredient.Quantity"
|
||||||
|
data-ingredient-name="@ingredient.Ingredient.Name"
|
||||||
|
data-unit="@(ingredient.Unit?.Symbol ?? string.Empty)">
|
||||||
|
<input type="checkbox" name="ingredientIds" value="@ingredient.Id" />
|
||||||
|
<span class="ingredient-name">@ingredient.Ingredient.Name</span> -
|
||||||
|
<span class="ingredient-quantity">@ingredient.Quantity</span>
|
||||||
|
<span class="ingredient-unit">@(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty)</span>
|
||||||
|
<span class="ingredient-note"></span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<button type="button" class="btn btn-primary mt-2" onclick="addSelectedIngredientsToShoppingList('@Model.Id')">Add Selected to Shopping List</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@model Francesco.Recipes.World.Models.InstructionViewModel
|
||||||
|
|
||||||
|
<div class="recipe-instructions-section">
|
||||||
|
<h3>Instructions</h3>
|
||||||
|
|
||||||
|
<div class="instructions-grid">
|
||||||
|
@foreach (var instruction in Model.Instructions.OrderBy(i => i.Number))
|
||||||
|
{
|
||||||
|
<div class="instruction-card">
|
||||||
|
<div class="instruction-image">
|
||||||
|
@if (instruction.MediaFiles != null && instruction.MediaFiles.Any())
|
||||||
|
{
|
||||||
|
var mediaFile = instruction.MediaFiles.First();
|
||||||
|
if (mediaFile.Data != null && mediaFile.Data.Length > 0)
|
||||||
|
{
|
||||||
|
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)" alt="Step @instruction.Number" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="placeholder-image">
|
||||||
|
<i class="bi bi-image"></i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<span class="step-number">@instruction.Number</span>
|
||||||
|
</div>
|
||||||
|
<p class="instruction-text">@instruction.Description</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -86,6 +86,106 @@ textarea.form-control {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.recipe-card {
|
.recipe-card {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|||||||
Reference in New Issue
Block a user