diff --git a/Francesco.Recipes.World/Constants/ContentType.cs b/Francesco.Recipes.World/Constants/ContentType.cs new file mode 100644 index 0000000..8f7d090 --- /dev/null +++ b/Francesco.Recipes.World/Constants/ContentType.cs @@ -0,0 +1,7 @@ +namespace Francesco.Recipes.World.Constants +{ + public class ContentType + { + public const string Image = "image/"; + } +} diff --git a/Francesco.Recipes.World/Constants/SortOrders.cs b/Francesco.Recipes.World/Constants/SortOrders.cs new file mode 100644 index 0000000..479261c --- /dev/null +++ b/Francesco.Recipes.World/Constants/SortOrders.cs @@ -0,0 +1,8 @@ +namespace Francesco.Recipes.World.Constants +{ + public class SortOrders + { + public const string Newest = "newest"; + public const string Oldest = "oldest"; + } +} diff --git a/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs b/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs new file mode 100644 index 0000000..9cd9c7a --- /dev/null +++ b/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs @@ -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 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); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs new file mode 100644 index 0000000..b7ba81b --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs @@ -0,0 +1,571 @@ +// +using System; +using Francesco.Recipes.World.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + [DbContext(typeof(FrancescosRecipesWorldDbContext))] + [Migration("20250602133408_RenameFavoriteColumn")] + partial class RenameFavoriteColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"), + Name = "Vorspeisen & Snacks" + }, + new + { + Id = new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), + Name = "Erste Gänge" + }, + new + { + Id = new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), + Name = "Hauptgerichte" + }, + new + { + Id = new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"), + Name = "Desserts & Süßspeisen" + }, + new + { + Id = new Guid("28e39168-701a-4084-81da-d96c987c462f"), + Name = "Beilagen & Salate" + }, + new + { + Id = new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"), + Name = "Kuchen" + }, + new + { + Id = new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), + Name = "Hefegebäck & Brot" + }, + new + { + Id = new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"), + Name = "Soßen & Saucen" + }, + new + { + Id = new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), + Name = "Marmeladen & Eingemachtes" + }, + new + { + Id = new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), + Name = "Getränke" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Favorits"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsChecked") + .HasColumnType("bit"); + + b.Property("RecipeIngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeIngredientId"); + + b.HasIndex("RecipeShoppingListId"); + + b.ToTable("RecipeIngredientsShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.ToTable("Instructions"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("InstructionId") + .HasColumnType("uniqueidentifier"); + + b.Property("MimeType") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("InstructionId"); + + b.HasIndex("RecipeId"); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CookingTime") + .HasColumnType("time"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Difficulty") + .HasColumnType("int"); + + b.Property("FavoriteId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsFavorite") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PreparationTime") + .HasColumnType("time"); + + b.Property("Servings") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("FavoriteId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UnitId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("UnitId"); + + b.ToTable("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.HasIndex("ShoppingListId"); + + b.ToTable("RecipeShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("ShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Units"); + + b.HasData( + new + { + Id = new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), + Name = "liter", + Symbol = "l" + }, + new + { + Id = new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), + Name = "gramm", + Symbol = "g" + }, + new + { + Id = new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), + Name = "kilogramm", + Symbol = "kg" + }, + new + { + Id = new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), + Name = "stücke", + Symbol = "stk" + }, + new + { + Id = new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), + Name = "blatt", + Symbol = "blatt" + }, + new + { + Id = new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), + Name = "messerspitze", + Symbol = "msp" + }, + new + { + Id = new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), + Name = "stange", + Symbol = "stange" + }, + new + { + Id = new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"), + Name = "bund", + Symbol = "bund" + }, + new + { + Id = new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), + Name = "zehe", + Symbol = "zehe" + }, + new + { + Id = new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), + Name = "teelöffel", + Symbol = "TL" + }, + new + { + Id = new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), + Name = "esslöffel", + Symbol = "EL" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null) + .WithMany("IngredientShoppingLists") + .HasForeignKey("IngredientId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient") + .WithMany() + .HasForeignKey("RecipeIngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") + .WithMany("SelectedIngredients") + .HasForeignKey("RecipeShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RecipeIngredient"); + + b.Navigation("RecipeShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("Instructions") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction") + .WithMany("MediaFiles") + .HasForeignKey("InstructionId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("MediaFiles") + .HasForeignKey("RecipeId"); + + b.Navigation("Instruction"); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category") + .WithMany("Recipes") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "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 + } + } +} diff --git a/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs new file mode 100644 index 0000000..79582f6 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + /// + public partial class RenameFavoriteColumn : Migration + { + /// + 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); + } + + /// + 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); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs index 5def152..67067f0 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -221,7 +221,7 @@ namespace Francesco.Recipes.World.Migrations b.Property("Difficulty") .HasColumnType("int"); - b.Property("FavoritId") + b.Property("FavoriteId") .HasColumnType("uniqueidentifier"); b.Property("IsFavorite") @@ -244,7 +244,7 @@ namespace Francesco.Recipes.World.Migrations b.HasIndex("CategoryId"); - b.HasIndex("FavoritId"); + b.HasIndex("FavoriteId"); b.ToTable("Recipes"); }); @@ -460,15 +460,15 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorite") .WithMany("Recipe") - .HasForeignKey("FavoritId") + .HasForeignKey("FavoriteId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Category"); - b.Navigation("Favorit"); + b.Navigation("Favorite"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs index 977b9ad..9e688a8 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs @@ -34,7 +34,7 @@ public class Recipe : ITimeStampedEntity public virtual ICollection MediaFiles { get; set; } = new List(); - public virtual Favorit Favorit { get; set; } = new (); + public virtual Favorit Favorite { get; set; } = new (); public virtual Category Category { get; set; } = new (); } diff --git a/Francesco.Recipes.World/Models/FavoriteViewModel.cs b/Francesco.Recipes.World/Models/FavoriteViewModel.cs new file mode 100644 index 0000000..b3412c6 --- /dev/null +++ b/Francesco.Recipes.World/Models/FavoriteViewModel.cs @@ -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 FavoriteRecipes { get; set; } = new List(); + + public string SortOrder { get; set; } = SortOrders.Newest; + + public bool HasFavorites => FavoriteRecipes.Any(); + + public string SortOrderDisplayText => SortOrder == SortOrders.Oldest ? "Älteste Favorits" : "Neueste Favorits"; + } +} diff --git a/Francesco.Recipes.World/Models/SearchViewModel.cs b/Francesco.Recipes.World/Models/SearchViewModel.cs new file mode 100644 index 0000000..c148d5e --- /dev/null +++ b/Francesco.Recipes.World/Models/SearchViewModel.cs @@ -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 Ingredients { get; set; } = new (); + + public TimeSpan TotalTime { get; set; } + } +} diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index 1caf8cf..60f7325 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -20,7 +20,8 @@ var connectionString = builder.Configuration.GetConnectionString("FrancescosReci ?? throw new InvalidOperationException("Connection string 'FrancescosRecipesWorldDbContextConnection' not found."); services.AddDbContext(options => - options.UseSqlServer(connectionString)); + options.UseSqlServer(connectionString, sqlOptions => + sqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery))); // Add services to the container. builder.Services.AddControllersWithViews(); diff --git a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs index 821f14f..e3e6d11 100644 --- a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs +++ b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs @@ -47,6 +47,7 @@ return await _context.Categories .Include(c => c.Recipes) .ThenInclude(r => r.MediaFiles) + .AsSplitQuery() .ToListAsync(); } } diff --git a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs index eb8cae2..5ca4fa3 100644 --- a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs +++ b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs @@ -16,8 +16,10 @@ public async Task> GetFavoriteRecipesAsync() { return await _context.Recipes - .Where(r => r.IsFavorite) - .ToListAsync(); + .Where(r => r.IsFavorite) + .Include(r => r.Favorite) + .Include(r => r.MediaFiles) + .ToListAsync(); } public async Task IsFavoriteAsync(Guid recipeId) @@ -28,12 +30,32 @@ public async Task AddFavoriteAsync(Guid recipeId) { - var recipe = await _context.Recipes.FindAsync(recipeId); - if (recipe != null && !recipe.IsFavorite) + var recipe = await _context.Recipes + .Include(r => r.Favorite) + .FirstOrDefaultAsync(r => r.Id == recipeId); + + if (recipe == null) { - recipe.IsFavorite = true; - await _context.SaveChangesAsync(); + throw new InvalidOperationException("Recipe not found."); } + + 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) diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs index d4a8d9a..d929250 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -1,5 +1,6 @@ namespace Francesco.Recipes.World.Repositories.MediaFile { + using Francesco.Recipes.World.Constants; using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models.BackendModels.MediaFile; using Francesco.Recipes.World.Models.BackendModels.Recipe; @@ -83,7 +84,7 @@ { 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/"); if (!isImage && !isVideo) @@ -93,7 +94,7 @@ if (isImage) { - await RemoveExistingMediaAsync(recipe, "image/"); + await RemoveExistingMediaAsync(recipe, ContentType.Image); } else { diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index 5afb2f0..ebbc8c7 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -1,5 +1,6 @@ namespace Francesco.Recipes.World.Repositories.Recipe { + using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Models.BackendModels.Category; using Francesco.Recipes.World.Models.BackendModels.Recipe; @@ -17,8 +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); + Task> SearchInRecipesAndIngredients(string searchTerm); } } diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index 73c76a7..3f8c683 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -1,6 +1,9 @@ namespace Francesco.Recipes.World.Repositories.Recipe { + using System.Linq.Expressions; + using Francesco.Recipes.World.Constants; using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Models.BackendModels.Category; using Francesco.Recipes.World.Models.BackendModels.Ingredient; using Francesco.Recipes.World.Models.BackendModels.Recipe; @@ -148,21 +151,32 @@ await _context.SaveChangesAsync(); } - public async Task> SearchInRecipesAndIngredients(string searchTerm) + public async Task> SearchInRecipesAndIngredients(string searchTerm) { - var queryable = _context.Recipes - .Include(r => r.RecipeIngredients) - .ThenInclude(ri => ri.Ingredient) - .AsQueryable(); - - if (!string.IsNullOrWhiteSpace(searchTerm)) + try { - searchTerm = searchTerm.ToLower(); - queryable = queryable.Where(r => r.Name.ToLower().Contains(searchTerm) || - r.RecipeIngredients.Any(ri => ri.Ingredient.Name.ToLower().Contains(searchTerm))); - } + if (string.IsNullOrWhiteSpace(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(); + } } public async Task> GetRecipesByDifficultyAsync(Difficulty? difficulty) @@ -184,6 +198,35 @@ .ToListAsync(); } + private static Expression> 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 ApplyRecipeSearchFilter(IQueryable 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 DeleteRecipeAsync(Guid recipeId) { var recipe = await GetRecipeByIdAsync(recipeId); diff --git a/Francesco.Recipes.World/Views/Favorite/Index.cshtml b/Francesco.Recipes.World/Views/Favorite/Index.cshtml new file mode 100644 index 0000000..b1b84af --- /dev/null +++ b/Francesco.Recipes.World/Views/Favorite/Index.cshtml @@ -0,0 +1,77 @@ +@model Francesco.Recipes.World.Models.FavoriteViewModel +@{ + ViewData["Title"] = "Favoriten"; +} + +

Favoriten

+ +
+ +
+ +@if (!Model.HasFavorites) +{ +
+ Keine Favoriten vorhanden. Füge Rezepte zu deinen Favoriten hinzu, indem du auf den Stern klickst. +
+} +else +{ +
+ @foreach (var recipe in Model.FavoriteRecipes) + { +
+
+ @{ + var mediaFile = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType != null && m.MimeType.StartsWith("image/")); + var imageData = mediaFile?.Data; + var mimeType = mediaFile?.MimeType; + } + +
+ @if (imageData != null && mimeType != null) + { + @recipe.Name + } + else + { +
Kein Bild
+ } +
+ +
+
@recipe.Name
+
+ + @await Html.PartialAsync("_FavoriteButton", recipe) + + + @(recipe.PreparationTime.TotalMinutes + recipe.CookingTime.TotalMinutes)min + +
+
+ +
+
+ } +
+} diff --git a/Francesco.Recipes.World/Views/Home/Index.cshtml b/Francesco.Recipes.World/Views/Home/Index.cshtml index 0c9e60c..031bd3b 100644 --- a/Francesco.Recipes.World/Views/Home/Index.cshtml +++ b/Francesco.Recipes.World/Views/Home/Index.cshtml @@ -1,45 +1,44 @@ @model IEnumerable -@Html.AntiForgeryToken() + @Html.AntiForgeryToken() -
- Willkommen -

Willkommen in der Rezept-App

-
+
+ Willkommen +

Willkommen in der Rezept-App

+
-
- -
+
+ +
-
+
-@foreach (var category in Model) -{ -
-
-

@category.Category.Name

- Alle @category.Category.Name-Rezepte anzeigen -
+ @foreach (var category in Model) + { +
+
+

@category.Category.Name

+ Alle @category.Category.Name-Rezepte anzeigen +
-
- @foreach (var recipe in category.Recipes) - { - var mediaFile = recipe.MediaFiles?.FirstOrDefault(); - var imageData = mediaFile?.Data; - var mimeType = mediaFile?.MimeType; +
+ @foreach (var recipe in category.Recipes) + { + var mediaFile = recipe.MediaFiles?.FirstOrDefault(); + var imageData = mediaFile?.Data; + var mimeType = mediaFile?.MimeType; - + } - -} - - + } diff --git a/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml b/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml index 4edbb64..c03a7fb 100644 --- a/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml +++ b/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml @@ -1,58 +1,75 @@ -@model IEnumerable +@model IEnumerable @{ if (!Model.Any()) -{ -

Keine Ergebnisse gefunden.

-} -else -{ -
- @foreach (var recipe in Model) - { -
-
-
- @{ - var mediaFile = recipe.MediaFiles.FirstOrDefault(); - - if (mediaFile?.Data != null) - { - @recipe.Name - } - else - { - Platzhalter - } - } -
- -
-
-
@recipe.Name
-

- - @recipe.PreparationTime.Hours h @recipe.PreparationTime.Minutes min -

+ { +

Keine Ergebnisse gefunden.

+ } + else + { +
+ @foreach (var recipe in Model) + { +
+
+
+ @if (recipe.ImageData != null && recipe.MimeType != null) + { + @recipe.Name + } + else + { + Platzhalter + }
-
- +
+
+
@recipe.Name
+

+ + @recipe.TotalTime.Hours h @recipe.TotalTime.Minutes min +

+
+ +
+
+
+ @Html.AntiForgeryToken() + + +
+
+ + Details +
-
- } -
+ } +
+ } } -} - diff --git a/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml b/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml index 5cd045a..7db2e11 100644 --- a/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml @@ -1,25 +1,26 @@ @model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe - +
+ @Html.AntiForgeryToken() + + +