Compare commits

...

220 Commits

Author SHA1 Message Date
Francesco Lorenzo D'Amico 9acd4a59ce Resolve merge conflicts 2026-07-17 14:18:59 +02:00
Francesco Lorenzo D'Amico 0adcd6c08a Initial commit 2026-07-17 14:06:58 +02:00
francesco448 be57ac7b65 Merge pull request #2 from francesco448/feature/Show-ShoppingList-Of-Active-RecipeCard
Feature/show shopping list of active recipe card
2026-03-02 16:39:41 +01:00
francesco448 b0519e81cf Merge pull request #3 from francesco448/feature-copy
Feature copy
2026-03-02 16:39:08 +01:00
Francesco Lorenzo D'Amico b546d8946e Finished Project 2026-03-02 16:35:32 +01:00
Francesco Lorenzo D'Amico cac83e38a8 Introduce IFavoritable and adopt across Recipe and view models 2026-03-02 15:59:48 +01:00
Francesco Lorenzo D'Amico de904e0a28 Simplify controllers: use repo view models, streamline favorites, add HTMX redirects 2026-03-02 15:54:04 +01:00
francesco448 5bcca4aafe Update README with project status and roadmap
Added project status, planned improvements, and technical stability sections to README.
2026-02-25 17:43:20 +01:00
francesco448 6a24ce43cc Update README with project overview and tech details
Erweiterte README mit detaillierten Informationen zur Architektur, Technologien und persönlichen Lernerfahrungen im Projekt.
2026-02-25 16:51:34 +01:00
francesco448 ef5c0476b0 Update project description in README
Enhanced project description to include database features.
2026-02-25 16:15:52 +01:00
francesco448 936c5c0428 Updating README.md with new project details and learnings. 2026-02-25 16:14:55 +01:00
Francesco Lorenzo D'Amico 247fbd6ae9 Merge develop into feature/Show-ShoppingList-Of-Active-RecipeCard 2025-06-04 10:59:20 +02:00
Francesco D'Amico 7661e9afd3 Merge branch 'feature/Favorite-Site' into 'develop'
Favorite-Site

See merge request francesco.damico/francescos.recipes.world!17
2025-06-03 12:34:12 +00:00
Francesco Lorenzo D'Amico 78dbfa20f1 Change Favorit to Favorite 2025-06-03 14:18:23 +02:00
Francesco Lorenzo D'Amico 9d90a54830 Refactor some code in one method 2025-06-03 14:18:14 +02:00
Francesco Lorenzo D'Amico a0e9e83118 Change magic strings with consts 2025-06-03 14:17:35 +02:00
Francesco Lorenzo D'Amico 3ae229b4f3 ChangeFavorit to Favorite and change some method logic 2025-06-03 14:17:35 +02:00
Francesco Lorenzo D'Amico 0f274613de Change magic strings with the consts 2025-06-03 14:17:35 +02:00
Francesco Lorenzo D'Amico 17726a4b63 Make new migration for renaming Favorit columns to Favorite 2025-06-03 14:17:35 +02:00
Francesco Lorenzo D'Amico febcdc1ced Change all magic strings with the consts 2025-06-03 14:17:35 +02:00
Francesco Lorenzo D'Amico 40690304fa Make const folder with all necessary constants 2025-06-03 14:17:35 +02:00
Francesco Lorenzo D'Amico e468a47782 Replaced DbContext registration to use SplitQuery mode 2025-06-03 14:17:35 +02:00
Francesco Lorenzo D'Amico 075aefc97d Make Favorite Site 2025-06-03 14:17:35 +02:00
Francesco Lorenzo D'Amico c20ed984c2 Replaced direct access to DB model properties with a dedicated ViewModel.
Added per-recipe HTMX form including AntiForgeryToken and dynamic star icon based on IsFavorite status.
2025-06-03 14:17:34 +02:00
Francesco Lorenzo D'Amico 28e60b79fa Refactor favorite button: wrap in form for better HTMX and AntiForgeryToken support 2025-06-03 14:17:34 +02:00
Francesco Lorenzo D'Amico 294f601085 Make some simple improvments 2025-06-03 14:17:34 +02:00
Francesco Lorenzo D'Amico e1dd694fb7 Optimize Searchfilter method 2025-06-03 14:17:25 +02:00
Francesco Lorenzo D'Amico ba61988840 Ensure that all favorites are retrieved, and when a recipe is added to favorites, include the creation date and time. 2025-06-03 14:16:24 +02:00
Francesco Lorenzo D'Amico 9ab1efde0e Use AsSplitQuery to optimize loading related data 2025-06-03 14:16:24 +02:00
Francesco Lorenzo D'Amico fcf03d0f62 Use AsSplitQuery for optimize loading related data 2025-06-03 14:16:24 +02:00
Francesco Lorenzo D'Amico 967542a0f1 Make a viewmodel for the main search 2025-06-03 14:16:24 +02:00
Francesco Lorenzo D'Amico c9f3f2b343 Make an viewmodel for the favorit sort 2025-06-03 14:16:24 +02:00
Francesco Lorenzo D'Amico bb049e674a Make an Endpoint for sort favorit to newsest and oldest 2025-06-03 14:16:24 +02:00
Francesco D'Amico 92c85da0c8 Merge branch 'feature/Recipe-Detail-Site' into 'develop'
Recipe-Detail-Site

See merge request francesco.damico/francescos.recipes.world!15
2025-06-03 08:21:44 +00:00
Francesco Lorenzo D'Amico 006ffc5ba3 Use exportet js functions 2025-06-02 12:35:04 +02:00
Francesco Lorenzo D'Amico 258ffb782f Move all methods from ShoppingListDetail to main JS and improve JS usage 2025-06-02 12:34:35 +02:00
Francesco Lorenzo D'Amico 6aeb7edc2b Build transaction on Method 2025-06-02 12:31:50 +02:00
Francesco Lorenzo D'Amico 2af526d623 Remove unnecessary id parameter 2025-06-02 12:31:18 +02:00
Francesco Lorenzo D'Amico c503e008a7 - remove blank line 2025-05-27 14:46:59 +02:00
Francesco Lorenzo D'Amico 907e19e398 Make the code more readable and easier with some style 2025-05-27 13:48:39 +02:00
Francesco Lorenzo D'Amico 5c9f6c8120 add comment for a specification of this code 2025-05-27 13:47:51 +02:00
Francesco Lorenzo D'Amico f2bb057a9b Change from HttpPost to HttpDelete 2025-05-27 13:47:13 +02:00
Francesco Lorenzo D'Amico 6290ceaaf0 Delete unnecessary using statemens 2025-05-27 12:21:20 +02:00
Francesco Lorenzo D'Amico dba048620b Add some style for Detail.cshtml 2025-05-27 12:19:31 +02:00
Francesco Lorenzo D'Amico b4b8062b13 Add recipe ingredient JSON serialization and dynamic function via JS 2025-05-27 12:18:37 +02:00
Francesco Lorenzo D'Amico 3b1d9fa4bc Delete unnecessary footer and add JS-Library of SweetAlert 2025-05-27 12:14:02 +02:00
Francesco Lorenzo D'Amico 8fe20c32db Add to the FAV button @Html.AntiForgeryToken 2025-05-27 12:12:36 +02:00
Francesco Lorenzo D'Amico f2b978f35f Remove unnecessary link arround the recipeCard and improve the Recipe Detail link 2025-05-27 12:10:39 +02:00
Francesco Lorenzo D'Amico ba0e35e526 Delete unnecessary Methods and add some better Methods that improves the process 2025-05-27 12:07:19 +02:00
Francesco Lorenzo D'Amico 203b8db180 Delete unnecessary property ShoppingList 2025-05-27 12:01:06 +02:00
Francesco Lorenzo D'Amico 5a5b3c5d87 Add some Endpoint for DeleteOperation and update API Endpoint (Detail) 2025-05-27 11:58:18 +02:00
Francesco Lorenzo D'Amico 963d9f4ab1 Change from HttpPost to HttpDelete 2025-05-27 11:56:26 +02:00
Francesco Lorenzo D'Amico b21f4ea9cd - 2025-05-20 16:12:06 +02:00
Francesco Lorenzo D'Amico b1a0a41202 Add Methodf to get all Shoopinglists on endpoint of the Detail Site of Shoppinglist 2025-05-20 16:05:03 +02:00
Francesco Lorenzo D'Amico 69affbdaed Add a List of Recipes that contains a Shoppinglist to show on ShoppingLIst detail site 2025-05-20 16:04:01 +02:00
Francesco Lorenzo D'Amico 222afcd84f Add Caroussel to Shoppinglistdetail Site 2025-05-20 15:59:19 +02:00
Francesco Lorenzo D'Amico 6600c9ab13 Add AntiForgeryToken with them stuff on js request 2025-05-20 15:53:15 +02:00
Francesco Lorenzo D'Amico e59b11b32f Make a Method to avoid reduntant code and add a similar method as Getting one Shoppinglist but this time gets All Shoppinglists 2025-05-20 15:51:18 +02:00
Francesco Lorenzo D'Amico 924b896a9a Remove empty line 2025-05-20 14:26:42 +02:00
Francesco Lorenzo D'Amico f89cf27be4 Add LIst instead of use an 2 dimensional array 2025-05-20 14:26:14 +02:00
Francesco Lorenzo D'Amico 06ef8927a7 Use one way of loading for avoid inconsistency 2025-05-20 14:25:19 +02:00
Francesco Lorenzo D'Amico 0c3b7380c5 Refactoring some Repo methods to not throw exception 2025-05-20 14:23:46 +02:00
Francesco Lorenzo D'Amico 8d9eb4b364 Everywhere an delete operation is name from HttpPost to HttpDelete 2025-05-20 14:22:28 +02:00
Francesco Lorenzo D'Amico dca567ba9f Show amount of shoppinglists in the view with dynamically change of amount 2025-05-19 13:28:26 +02:00
Francesco Lorenzo D'Amico 385be506a7 Implement a method to get whole shoppinglist model 2025-05-19 13:25:35 +02:00
Francesco Lorenzo D'Amico 00cd006bf8 Create an initial ViewModel for the ShoppingList Detail 2025-05-19 13:24:38 +02:00
Francesco Lorenzo D'Amico 9c9253c94a Implement endpoint to fetch shopping list details by ID including total recipe count 2025-05-19 13:23:44 +02:00
franc 3fe1948867 remove Service Folder 2025-05-14 12:01:59 +02:00
franc a5b70532e6 rest 2025-05-14 12:01:31 +02:00
franc da2fd1e61d change redirection to index 2025-05-14 12:01:20 +02:00
franc 1443284236 Add bootastrap for icon by darkMode and LightMode 2025-05-14 12:00:54 +02:00
franc e470dd11bb Make a partial view for adjust amount of ingredient 2025-05-14 11:57:03 +02:00
franc bfbfd34624 Create Grid for show instructions of recipe 2025-05-14 11:47:29 +02:00
franc aa6f27383f Move the Ingredient List stuff to a Partial view 2025-05-14 11:46:48 +02:00
franc 325120e5ec Add style to Instructions Grid Layout 2025-05-13 14:52:16 +02:00
franc a57ebd4507 Adjust endpoint path for navigation 2025-05-13 14:46:59 +02:00
franc 1c47b770b4 Add Method to delete whole recipe with their relations and include Mediafiles to Instruction by getting recipe 2025-05-13 14:45:39 +02:00
franc ff08b82611 1.) Add an eindpoint for deleting recipe with their connections
2.) Add Endpoint for adjust Ingredient Amount
2025-05-13 14:44:18 +02:00
Christian Hunziker 98dd9a049b Merge branch 'feature/Create-Recipe' into 'develop'
Create-Recipe

See merge request francesco.damico/francescos.recipes.world!14
2025-05-07 08:06:28 +00:00
franc 27d4fd5368 Solve Thread of mergeRequest 2025-05-07 10:03:02 +02:00
franc 7f6998c2de delete unnecessary view 2025-05-07 09:00:05 +02:00
franc 03932b380d change some things 2025-05-07 08:59:43 +02:00
franc a7cafb479b Use normal List instead interface list 2025-05-07 08:59:23 +02:00
franc 12b217ea59 Rename Method 2025-05-07 08:56:00 +02:00
franc 7bf43b1855 Change some things 2025-05-07 08:55:40 +02:00
franc 070df12049 Add new Attribut 2025-05-07 08:55:04 +02:00
franc d172642b1e Add an new Attribut 2025-05-07 08:54:18 +02:00
franc abb1d774fe Solve all thread from mergeRequest to RecipeController 2025-05-07 08:53:12 +02:00
franc 7168d93621 Rename Methode 2025-05-05 17:12:06 +02:00
franc a0d05dcdc2 change from ienumerable to list 2025-05-05 17:11:18 +02:00
franc 4928a3cc7d Move css style on a separate .css file 2025-05-05 17:10:40 +02:00
franc dc9a1f1b9c Move script section stuff that is implementet on more than one script section on layout 2025-05-05 17:10:05 +02:00
franc 402f5f2fe9 Rename method 2025-05-05 17:08:17 +02:00
franc 0ff81912a9 Rename Method from CreateInstructionWithImageToRecipeAsync to CreateInstructionAsync 2025-05-05 17:07:39 +02:00
franc a0044a8d30 Check recipeId null 2025-05-05 17:05:30 +02:00
franc a2e50a0195 Move all js scripts into this file 2025-05-05 12:33:05 +02:00
franc 1e6d354343 Remove js an placed to site.js 2025-05-05 12:32:32 +02:00
franc a7a5650046 Make a completly new Create view with some css style, that contains all PartialViews 2025-05-05 12:30:53 +02:00
franc 594315c473 Make link to page for create Recipe instead to show the form dynamically by button click 2025-05-05 12:29:19 +02:00
franc 1a91ec38dd Avoid datainconsistency 2025-05-05 12:26:19 +02:00
franc a76f757081 - Forgive an IFormFile Parameter for CreateInstructionWithImageToRecipeAsync
-Refactor instruction number generation to simplify null-check and remove redundant increment
- By creating an InstructionFile put Recipe Id to null for avoid Datainconsistency
2025-05-05 12:24:20 +02:00
franc 080c4eda78 Create ViewModels for the Create Recipe logic 2025-05-05 12:19:27 +02:00
franc 0d57d40651 Make Instruction and Recipe nullable for avoid data inconsistency 2025-05-05 12:18:22 +02:00
franc d131ec1d16 Implement a new endpoint to retrieve all units for a dynamic dropdown. 2025-05-05 12:17:06 +02:00
franc 000cead877 - Replaced foreach loop with for loop for better index-based access to instructions.
- Added logic to dynamically retrieve media files for each instruction using `Request.Form.Files`.
- Ensured that media files are correctly associated with their respective instructions during recipe creation.
2025-05-05 12:01:53 +02:00
franc 7976545da6 Add Method for uploading Instruction Image to Controller and put ValidateAntiForgeryKey instead of AutoValidateAntiForgeryKey 2025-05-05 11:56:15 +02:00
franc ca15b69677 create a js file with all scripts 2025-04-30 18:54:38 +02:00
franc 8b3489cf63 Add viewModels for creatingRecipes, Instruction and ingredient of Recipe 2025-04-30 18:53:20 +02:00
franc 9e7ff56099 Make New Migration to make Instruction nullable for avoid data inconsistenc 2025-04-30 18:51:36 +02:00
Francesco D'Amico 61f3e27738 Merge branch 'feature/ShoppingList-Logic' into 'develop'
ShoppingList Logic

See merge request francesco.damico/francescos.recipes.world!8
2025-04-29 07:21:41 +00:00
franc a8fdca4871 Rename refactoring method 2025-04-29 09:16:18 +02:00
franc 327ba7b1cc delete unnecessary service for shoppinglist 2025-04-29 09:04:42 +02:00
franc 3ca5b35cfe Change some method logic 2025-04-29 09:04:18 +02:00
Francesco D'Amico 12aab06874 Merge branch 'feature/Sort-Logic-Instruction' into 'develop'
Feature/sort logic instruction

See merge request francesco.damico/francescos.recipes.world!9
2025-04-28 14:35:42 +00:00
franc 5848697dec Use model instead of view data 2025-04-28 16:06:31 +02:00
franc 93162f3a4b using model instead of ViewData 2025-04-28 16:06:01 +02:00
franc c001fee09d Comment code 2025-04-28 16:04:29 +02:00
franc d3a74196f2 Use new method in controller 2025-04-28 15:02:20 +02:00
franc be211b12c6 Make methode to get SortedInstruction 2025-04-28 15:01:59 +02:00
franc 8dc53201ce Create InstructionViewModel 2025-04-28 15:01:13 +02:00
franc aa5cc3d683 Use InstructionViewModel 2025-04-28 15:00:50 +02:00
franc e6a9e92748 Rename refactoring Method 2025-04-28 13:36:40 +02:00
franc 9c1584d621 Make two views for sort logic one for show the instructions and the other for add Instruction 2025-04-28 13:13:24 +02:00
franc 2f879d987b Add more methods for improve sort logic 2025-04-28 13:11:28 +02:00
franc bcc0a60669 Update Controller stuff with Repo Methods 2025-04-28 13:10:41 +02:00
franc e169e94c8e Solve format 2025-04-23 13:40:27 +02:00
franc cc30f5b149 Conflict resolve 2025-04-23 13:37:22 +02:00
franc 7035a9b64d Add also here recipeId 2025-04-23 13:33:33 +02:00
franc 146cdf9bff Remove unnecessary context dependency 2025-04-23 13:32:18 +02:00
franc 0d1f18ecf6 Just add scope for InstructionService 2025-04-23 13:28:24 +02:00
franc 0859e3aa2b Add recipeid to the methods seperate the Id logic between instructionId and recipeId 2025-04-23 13:27:51 +02:00
franc 1bdb14e9d3 Rename Refactoring 2025-04-23 11:51:12 +02:00
franc 059e93cb86 Remove Blank line 2025-04-23 11:51:12 +02:00
franc 32aaec7d65 Remove the interface declaration for MoveInstructionAsync since it's now a private method 2025-04-23 11:51:12 +02:00
franc 825818645a Refactoring some code and change logic and make sure to reduce db calls and avoid redundants 2025-04-23 11:51:12 +02:00
franc d2560e2971 Implement the Sorting Logic 2025-04-23 11:51:12 +02:00
franc 0fb9333b82 Implement two methods for the sorting logic on the service 2025-04-23 11:51:12 +02:00
Christian Hunziker 52b4e68fc6 Merge branch 'feature/HomePage-With-Categories-their-Recipes' into 'develop'
Feature/home page with categories their recipes

See merge request francesco.damico/francescos.recipes.world!13
2025-04-23 09:27:22 +00:00
franc 12752c4834 Delete unecessary Route 2025-04-23 11:26:14 +02:00
franc 16a9d10844 Return PartialView instead an Redirection 2025-04-22 14:49:29 +02:00
franc 323af8d6f0 Create new folder for images and put one svg for favorite symbol 2025-04-22 14:42:46 +02:00
franc 1239f31673 add HTMX CSRF token header support 2025-04-22 14:42:04 +02:00
franc 5f92625c16 Make a PartialView for favorite System 2025-04-22 14:40:25 +02:00
franc cc590fa65c Make a rude Index Home Page with a bit style 2025-04-22 14:38:57 +02:00
franc 71c9f27d64 Including MediaFile of Recipe for show it on card 2025-04-22 14:37:19 +02:00
franc cbd2ba7dfb use ViewModel in Index action instead of raw entities 2025-04-22 14:32:19 +02:00
Dömu Brunner 9ed8158135 Merge branch 'feature/filter-Recipe-and-Ingredient' into 'develop'
filter-Recipe-and-Ingredient

See merge request francesco.damico/francescos.recipes.world!10
2025-04-17 09:43:31 +00:00
franc 35f2d1675d Rename Refactoring 2025-04-17 11:41:20 +02:00
franc 4eaf03698c Rename refactoring 2025-04-17 11:20:02 +02:00
franc df146a24b0 Rename Refactoring 2025-04-17 11:15:15 +02:00
franc 0d6b9eb294 Rename refactoring 2025-04-17 10:49:40 +02:00
franc 71ac7eb03f Rename Refactoring 2025-04-17 10:34:12 +02:00
franc 556b22b8df Rename Refactoring 2025-04-17 10:24:08 +02:00
franc c225eedc8b Fix some formatting and Rename Refactoring 2025-04-17 10:20:55 +02:00
franc c07cdf7eae Move Method Body on Repositroy and inject Repository on Service 2025-04-16 17:37:11 +02:00
franc 032e2caf1b Remove Blank line 2025-04-16 17:27:41 +02:00
franc b7706de30c Remove the interface declaration for MoveInstructionAsync since it's now a private method 2025-04-16 17:27:08 +02:00
franc 4b084eea98 Refactoring some code and change logic and make sure to reduce db calls and avoid redundants 2025-04-16 17:22:19 +02:00
franc 1110c74234 Clean the code and put Index EndPoint 2025-04-16 15:19:33 +02:00
franc b8814ce708 Correct the Method and rename Method 2025-04-16 15:18:39 +02:00
franc eb6d82a979 Add HomeController with search endpoint for querying recipes 2025-04-16 11:22:09 +02:00
franc c8ff0dd9dd Make a searchResultPartial and initial HomePage with filter system 2025-04-16 11:20:33 +02:00
franc 3ec9d4be79 Create main Layout for Application/Web-Side 2025-04-16 11:18:10 +02:00
franc 2f33951c4e Change Search Logic 2025-04-16 11:17:42 +02:00
franc 84c75098cf Put one Method to Count all ShoppingListsRecipe 2025-04-15 16:20:08 +02:00
franc 71ebfcf4be Implement the Sorting Logic 2025-04-15 16:11:07 +02:00
franc 7d11eaf7ef Implement two methods for the sorting logic on the service 2025-04-15 16:10:42 +02:00
Francesco D'Amico f64f1bf1ee Merge branch 'feature/Implement_All_Repositories' into 'develop'
Repository Stuff

See merge request francesco.damico/francescos.recipes.world!5
2025-04-14 11:15:17 +00:00
franc 603dc4a2c6 All thread resolved 2025-04-10 10:23:43 +02:00
franc 127a60b621 Refactoring some Code and clean Code 2025-04-09 15:37:44 +02:00
franc d248ccb1db Reapply "add CategoryController with endpoints"
This reverts commit 928743303c.
2025-04-08 15:52:32 +02:00
franc 928743303c Revert "add CategoryController with endpoints"
This reverts commit 36bca4d658.
2025-04-08 15:23:57 +02:00
franc 36bca4d658 add CategoryController with endpoints 2025-04-08 15:22:28 +02:00
franc 712b561d12 add shoppinglist managment for ingredient 2025-04-08 15:06:50 +02:00
franc ddd0cad36b add recipe managment 2025-04-08 15:06:23 +02:00
franc 31fbafe806 add repository logic for mediafile 2025-04-08 15:05:49 +02:00
franc d4bcf7980b add instruction management for recipes 2025-04-08 15:05:15 +02:00
franc ffebc376e1 implement ingredient management methods 2025-04-08 14:30:12 +02:00
franc e438e8a114 implement FavoritRepository for managing favorite recipes 2025-04-08 14:25:21 +02:00
Francesco D'Amico c7543f8a49 Edit Program and csproj file 2025-04-02 08:46:37 +02:00
Francesco D'Amico 1fcf1a1c26 FilterByDifficulty testing 2025-04-01 17:02:47 +02:00
Francesco D'Amico 2acb4b4117 change some logic to the repositories 2025-03-30 15:56:39 +02:00
Francesco D'Amico c48d1bb5b2 make views for testing data 2025-03-30 15:56:17 +02:00
Francesco D'Amico b2ba51d297 update model 2025-03-30 15:56:01 +02:00
Francesco D'Amico afde272d8a Make new Migration and change logic of ingredientshoppinglist 2025-03-30 15:55:19 +02:00
Francesco D'Amico a4c8edb572 Write Controller for testing Data 2025-03-30 15:53:26 +02:00
Dömu Brunner 5c94402c2f Merge branch 'feature/Project_Config' into 'develop'
Add some configuration to Project

See merge request francesco.damico/francescos.recipes.world!4
2025-03-24 08:44:29 +00:00
Francesco D'Amico 41f41ce3fb Fixed errors SA1516 2025-03-24 09:27:13 +01:00
Francesco D'Amico 02533e9bfb Last files for merge 2025-03-19 16:33:40 +01:00
Francesco D'Amico b9d0bf2170 Edit .editconfig file 2025-03-19 16:28:12 +01:00
Francesco D'Amico 29faf5a99e Changed xml file on .gitignore 2025-03-19 15:50:33 +01:00
Francesco D'Amico ce2a5aceb2 update Microsoft.CodeAnalysis.NetAnalyzers to 9.0.0 2025-03-19 14:32:33 +01:00
Francesco D'Amico 906d26c2ec Edit .gitignore file 2025-03-19 14:22:55 +01:00
Francesco D'Amico 5622e55441 Add some configuration to Project 2025-03-19 13:56:21 +01:00
Christian Hunziker 1a8a0524a6 Merge branch 'feature/Create-Database-with-Models' into 'develop'
Feature/create database with models

See merge request francesco.damico/francescos.recipes.world!3
2025-03-19 09:55:42 +00:00
Francesco D'Amico d1b8c3c7e0 Fix Formatting 2025-03-19 10:52:01 +01:00
Francesco D'Amico c498e23a07 Correct models unit and recipe and add a new migration 2025-03-18 16:14:01 +01:00
Francesco D'Amico a91ca743ab Revert "Correct Models Unit and Category"
This reverts commit 45702aa4f2.
2025-03-18 15:45:14 +01:00
Francesco D'Amico dfd2d2ee3f Delete unnecessary files 2025-03-18 15:29:11 +01:00
Francesco D'Amico 6125eb82ee Correct Models 2025-03-18 15:25:30 +01:00
Francesco D'Amico d53729f798 Corret Models Unit and Category 2025-03-18 14:20:53 +01:00
Francesco D'Amico 45702aa4f2 Correct Models Unit and Category 2025-03-18 13:54:37 +01:00
Francesco D'Amico 36e107d48e Fixed some properties on models 2025-03-18 13:15:08 +01:00
Francesco D'Amico 449533cf0b Create all Models 2025-03-18 12:02:01 +01:00
Francesco D'Amico 6affee7b8c Initial Migration 2025-03-05 17:29:37 +01:00
Francesco D'Amico 32bc018c75 Merge branch 'feature/Create-Database-with-Models' into 'main'
Feature/create database with models

See merge request francesco.damico/francescos.recipes.world!1
2025-01-15 08:44:25 +00:00
Francesco D'Amico b06e1b27dd Have deleted unnecessary configurations 2025-01-14 14:27:31 +01:00
Francesco D'Amico f1af18f4fb To make clean the migrations i removed the recently migrations and have added a new migration. 2025-01-14 14:26:56 +01:00
Francesco D'Amico 08c567b9ba Some Configuration for this Project 2025-01-08 11:30:50 +01:00
Francesco D'Amico 2802030de1 Remove Identity cuz of no usage at the moment 2025-01-08 11:29:30 +01:00
Francesco D'Amico 53bf98ece6 Remove unnecessary method 2025-01-08 11:28:46 +01:00
Francesco D'Amico 1f2344eabf Add new Model Unit and changed some Logik to some Properties 2025-01-08 11:27:07 +01:00
Francesco D'Amico 6e9ce70355 Have made an migration and a DB context to make sure it does create all necessary tables 2025-01-06 17:01:30 +01:00
Francesco D'Amico b80a402d15 Create Models for this Project 2025-01-06 17:00:05 +01:00
Francesco D'Amico 9ac2428a1d Update .editorconfig 2024-12-10 13:03:47 +00:00
Francesco D'Amico 87b0a9993c Delete obsolete files 2024-12-10 13:39:51 +01:00
Francesco D'Amico a6e4353e97 Add missing blank line. 2024-12-10 13:38:33 +01:00
Francesco D'Amico 84a9cec80f Move usings into namespace 2024-12-10 13:33:19 +01:00
Francesco D'Amico 59626f852e InitialCommit 2024-12-04 11:27:03 +01:00
Francesco D'Amico 70df3c5aab Initial commit 2024-12-04 10:02:31 +00:00
169 changed files with 84251 additions and 4 deletions
+152
View File
@@ -0,0 +1,152 @@
# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
###############################
# Core EditorConfig Options #
###############################
root = true
# All files
[*]
indent_style = space
# Code files
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
charset = utf-8-bom
###############################
# .NET Coding Conventions #
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
# this. preferences
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
dotnet_style_readonly_field = true:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
# SA1201: Elements should appear in the correct order
dotnet_diagnostic.SA1201.severity = silent
###############################
# Naming Conventions #
###############################
# Style Definitions
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
###############################
# C# Coding Conventions #
###############################
[*.cs]
# var preferences
csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = true:warning
# Expression-bodied members
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
# Null-checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
# Expression-level preferences
csharp_prefer_braces = true:silent
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
###############################
# C# Formatting Rules #
###############################
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Space preferences
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
# SA1601: Partial elements should be documented
dotnet_diagnostic.SA1601.severity = none
# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.DocumentationRules'
dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = none
# SA1516: Elements should be separated by blank line
dotnet_diagnostic.SA1516.severity = error
# SA1649: File name should match first type name
dotnet_diagnostic.SA1649.severity = none
# CA1062: Validate arguments of public methods
dotnet_diagnostic.CA1062.severity = error
# SA0001: XML comment analysis is disabled
dotnet_diagnostic.SA0001.severity = none
# SA1309: Make sure variables with private access modifier are allowed to start with "_"
dotnet_diagnostic.SA1309.severity = none
# SA1309: Make sure class members are allowed to call without "this" prefix
dotnet_diagnostic.SA1101.severity = none
# SA1118: parameter spans multiple lines
dotnet_diagnostic.SA1118.severity = none
# SA1011: closing square bracket spacing
dotnet_diagnostic.SA1011.severity = none
# SA1517: no blank lines at start of file
dotnet_diagnostic.SA1517.severity = none
# SA1200: using directive should appear within a namespace
dotnet_diagnostic.SA1200.severity = none
+1 -3
View File
@@ -1,4 +1,3 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
@@ -83,8 +82,6 @@ StyleCopReport.xml
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
@@ -399,4 +396,5 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
src/Francesco.Recipes.World.xml
+14
View File
@@ -0,0 +1,14 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["Francesco.Recipes.World/Francesco.Recipes.World.csproj", "Francesco.Recipes.World/"]
RUN dotnet restore "Francesco.Recipes.World/Francesco.Recipes.World.csproj"
COPY . .
WORKDIR "/src/Francesco.Recipes.World"
RUN dotnet publish "Francesco.Recipes.World.csproj" -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "Francesco.Recipes.World.dll"]
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="16.0">
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
<Name Resource="MinimumRecommendedRules_Name" />
<Description Resource="MinimumRecommendedRules_Description" />
</Localization>
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
<Rule Id="CA1001" Action="Warning" />
<Rule Id="CA1009" Action="Warning" />
<Rule Id="CA1016" Action="Warning" />
<Rule Id="CA1033" Action="Warning" />
<Rule Id="CA1049" Action="Warning" />
<Rule Id="CA1060" Action="Warning" />
<Rule Id="CA1061" Action="Warning" />
<Rule Id="CA1063" Action="Warning" />
<Rule Id="CA1065" Action="Warning" />
<Rule Id="CA1301" Action="Warning" />
<Rule Id="CA1400" Action="Warning" />
<Rule Id="CA1401" Action="Warning" />
<Rule Id="CA1403" Action="Warning" />
<Rule Id="CA1404" Action="Warning" />
<Rule Id="CA1405" Action="Warning" />
<Rule Id="CA1410" Action="Warning" />
<Rule Id="CA1415" Action="Warning" />
<Rule Id="CA1821" Action="Warning" />
<Rule Id="CA1900" Action="Warning" />
<Rule Id="CA1901" Action="Warning" />
<Rule Id="CA2002" Action="Warning" />
<Rule Id="CA2100" Action="Warning" />
<Rule Id="CA2101" Action="Warning" />
<Rule Id="CA2108" Action="Warning" />
<Rule Id="CA2111" Action="Warning" />
<Rule Id="CA2112" Action="Warning" />
<Rule Id="CA2114" Action="Warning" />
<Rule Id="CA2116" Action="Warning" />
<Rule Id="CA2117" Action="Warning" />
<Rule Id="CA2122" Action="Warning" />
<Rule Id="CA2123" Action="Warning" />
<Rule Id="CA2124" Action="Warning" />
<Rule Id="CA2126" Action="Warning" />
<Rule Id="CA2131" Action="Warning" />
<Rule Id="CA2132" Action="Warning" />
<Rule Id="CA2133" Action="Warning" />
<Rule Id="CA2134" Action="Warning" />
<Rule Id="CA2137" Action="Warning" />
<Rule Id="CA2138" Action="Warning" />
<Rule Id="CA2140" Action="Warning" />
<Rule Id="CA2141" Action="Warning" />
<Rule Id="CA2146" Action="Warning" />
<Rule Id="CA2147" Action="Warning" />
<Rule Id="CA2149" Action="Warning" />
<Rule Id="CA2200" Action="Warning" />
<Rule Id="CA2202" Action="Warning" />
<Rule Id="CA2207" Action="Warning" />
<Rule Id="CA2212" Action="Warning" />
<Rule Id="CA2213" Action="Warning" />
<Rule Id="CA2214" Action="Warning" />
<Rule Id="CA2216" Action="Warning" />
<Rule Id="CA2220" Action="Warning" />
<Rule Id="CA2229" Action="Warning" />
<Rule Id="CA2231" Action="Warning" />
<Rule Id="CA2232" Action="Warning" />
<Rule Id="CA2235" Action="Warning" />
<Rule Id="CA2236" Action="Warning" />
<Rule Id="CA2237" Action="Warning" />
<Rule Id="CA2238" Action="Warning" />
<Rule Id="CA2240" Action="Warning" />
<Rule Id="CA2241" Action="Warning" />
<Rule Id="CA2242" Action="Warning" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">
<Rule Id="CS0618" Action="Info" />
<Rule Id="CS8019" Action="Warning" />
<Rule Id="CS8020" Action="Info" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeQuality.Analyzers" RuleNamespace="Microsoft.CodeQuality.Analyzers">
<Rule Id="CA1716" Action="Info" />
<Rule Id="CA1724" Action="Info" />
<Rule Id="CA2007" Action="Hidden" />
<Rule Id="CA2227" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.NetCore.Analyzers" RuleNamespace="Microsoft.NetCore.Analyzers">
<Rule Id="CA1303" Action="None" />
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<Rule Id="SA0001" Action="None" />
<Rule Id="SA1101" Action="None" />
<Rule Id="SA1309" Action="None" />
<Rule Id="SA1600" Action="None" />
<Rule Id="SA1602" Action="None" />
<Rule Id="SA1633" Action="None" />
</Rules>
</RuleSet>
+31
View File
@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35222.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Francesco.Recipes.World", "Francesco.Recipes.World\Francesco.Recipes.World.csproj", "{4D1BBCF4-8E06-4584-A383-B14BEC558408}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrancescosRecipeWorld Mock", "FrancescosRecipeWorld Mock\FrancescosRecipeWorld Mock.csproj", "{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Release|Any CPU.Build.0 = Release|Any CPU
{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {537F0DB8-C26E-4753-917D-0E9D6ED5FB3A}
EndGlobalSection
EndGlobal
@@ -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,39 @@
namespace Francesco.Recipes.World.Controller.Category
{
using Francesco.Recipes.World.Repositories.Category;
using Microsoft.AspNetCore.Mvc;
public class CategoryController : Controller
{
private readonly ICategoryRepository _categoryRepository;
public CategoryController(ICategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
}
// GET: /Category
[HttpGet]
public async Task<IActionResult> Index()
{
var categories = await _categoryRepository.GetAllCategoriesAsync();
return View(categories);
}
// GET: /Category/{id}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Details(Guid id)
{
var category = await _categoryRepository.GetCategoryByIdAsync(id);
return View(category);
}
// GET: /Category/{id}/recipes
[HttpGet("{id:guid}/recipes")]
public async Task<IActionResult> GetRecipesByCategory(Guid id)
{
var recipes = await _categoryRepository.GetRecipesByCategoryAsync(id);
return Ok(recipes);
}
}
}
@@ -0,0 +1,34 @@
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;
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);
}
}
}
@@ -0,0 +1,32 @@
namespace Francesco.Recipes.World.Controller.HomeController
{
using Francesco.Recipes.World.Repositories.Category;
using Francesco.Recipes.World.Repositories.Recipe;
using Microsoft.AspNetCore.Mvc;
public class HomeController : Controller
{
private readonly IRecipeRepository _recipeRepository;
private readonly ICategoryRepository _categoryRepository;
public HomeController(ICategoryRepository categoryRepository, IRecipeRepository recipeRepository)
{
_recipeRepository = recipeRepository;
_categoryRepository = categoryRepository;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var categoriesWithRecipes = await _categoryRepository.GetAllCategoriesWithRecipesViewModelAsync();
return View(categoriesWithRecipes);
}
[HttpGet("/Home/Search")]
public async Task<IActionResult> Search(string term)
{
var recipes = await _recipeRepository.SearchInRecipesAndIngredients(term);
return PartialView("_SearchResultsPartial", recipes);
}
}
}
@@ -0,0 +1,6 @@
namespace Francesco.Recipes.World.Controller.Ingredient
{
public class IngredientController
{
}
}
@@ -0,0 +1,69 @@
namespace Francesco.Recipes.World.Controller.Instruction
{
using Francesco.Recipes.World.Models;
using Francesco.Recipes.World.Repositories.Instruction;
using Francesco.Recipes.World.Services.Instruction;
using Microsoft.AspNetCore.Mvc;
public class InstructionController : Controller
{
private readonly IInstructionService _instructionService;
private readonly IInstructionRepository _instructionRepository;
public InstructionController(IInstructionService instructionService, IInstructionRepository instructionRepository)
{
_instructionService = instructionService;
_instructionRepository = instructionRepository;
}
[HttpPost("Recipe/{recipeId}/Instruction/{instructionId}/move-up")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> MoveUp(Guid recipeId, Guid instructionId)
{
try
{
await _instructionService.MoveInstructionUpAsync(recipeId, instructionId);
return Ok(new { Message = "Instruction moved up successfully." });
}
catch (Exception ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpPost("Recipe/{recipeId}/Instruction/{instructionId}/move-down")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> MoveDown(Guid recipeId, Guid instructionId)
{
try
{
await _instructionService.MoveInstructionDownAsync(recipeId, instructionId);
return Ok(new { Message = "Instruction moved down successfully." });
}
catch (Exception ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("Recipe/{recipeId}/Instructions")]
public async Task<IActionResult> GetInstructions(Guid recipeId)
{
try
{
var sortedInstructions = await _instructionService.GetSortedInstructionsAsync(recipeId);
var viewModel = new InstructionViewModel
{
RecipeId = recipeId,
Instructions = sortedInstructions,
};
return View("~/Views/Shared/_GetInstructions.cshtml", viewModel);
}
catch (Exception ex)
{
return BadRequest(new { Error = ex.Message });
}
}
}
}
@@ -0,0 +1,97 @@
namespace Francesco.Recipes.World.Controller.MediaFile
{
using Francesco.Recipes.World.Repositories.MediaFile;
using Microsoft.AspNetCore.Mvc;
[Route("Category/{categoryId}/Recipe")]
public class MediaFileController : Controller
{
private readonly IMediaFileRepository _mediaFileRepository;
public MediaFileController(IMediaFileRepository mediaFileRepository)
{
_mediaFileRepository = mediaFileRepository;
}
// POST: /UploadImage
[HttpPost("UploadImage")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadImage(Guid recipeId, IFormFile? mediaFile)
{
if (mediaFile is null)
{
return BadRequest("Photo is required.");
}
try
{
await _mediaFileRepository.UploadRecipeMediaAsync(recipeId, mediaFile);
return Ok("Image uploaded successfully.");
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
// GET: /UploadImage
[HttpGet("UploadImage")]
public IActionResult UploadImageView()
{
return View();
}
[HttpPost("ReplaceInstructionImage")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ReplaceInstructionImage(Guid instructionId, Guid mediaFileIdToReplace, IFormFile? newPhoto)
{
if (newPhoto is null)
{
return BadRequest("Photo is required.");
}
using (var memoryStream = new MemoryStream())
{
await newPhoto.CopyToAsync(memoryStream);
var newMediaData = memoryStream.ToArray();
try
{
await _mediaFileRepository.ReplaceInstructionImageAsync(instructionId, mediaFileIdToReplace, newPhoto.FileName, newPhoto.ContentType, newMediaData);
return Ok("Image replaced successfully.");
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
}
[HttpPost("Recipe/{recipeId}/Instruction/{instructionId}/UploadImage")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadInstructionImage(Guid recipeId, Guid instructionId, IFormFile? photo)
{
if (recipeId == Guid.Empty)
{
return BadRequest("Recipe ID is required.");
}
if (photo == null)
{
return BadRequest("Photo is required.");
}
try
{
await _mediaFileRepository.UploadInstructionImageAsync(instructionId, photo);
return RedirectToAction("GetInstructions", "Instruction", new { recipeId });
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
}
}
@@ -0,0 +1,523 @@
namespace Francesco.Recipes.World.Controller.Recipe
{
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models;
using Francesco.Recipes.World.Models.BackendModels.Ingredient;
using Francesco.Recipes.World.Models.BackendModels.Instruction;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Repositories.Category;
using Francesco.Recipes.World.Repositories.Favorit;
using Francesco.Recipes.World.Repositories.Ingredient;
using Francesco.Recipes.World.Repositories.Instruction;
using Francesco.Recipes.World.Repositories.MediaFile;
using Francesco.Recipes.World.Repositories.Recipe;
using Francesco.Recipes.World.Repositories.Unit;
using Francesco.Recipes.World.Views.Recipe;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
public class RecipeController : Controller
{
private readonly IRecipeRepository _recipeRepository;
private readonly IUnitRepository _unitRepository;
private readonly ICategoryRepository _categoryRepository;
private readonly IIngredientRepository _ingredientRepository;
private readonly IMediaFileRepository _mediaFileRepository;
private readonly IInstructionRepository _instructionRepository;
private readonly IFavoriteRepository _favoriteRepository;
private readonly FrancescosRecipesWorldDbContext _context;
public IReadOnlyCollection<Recipe> Recipes { get; set; }
public RecipeController(
IRecipeRepository recipeRepository,
IUnitRepository unitRepository,
ICategoryRepository categoryRepository,
IIngredientRepository ingredientRepository,
IMediaFileRepository mediaFileRepository,
IInstructionRepository instructionRepository,
IFavoriteRepository favoriteRepository,
FrancescosRecipesWorldDbContext context)
{
_recipeRepository = recipeRepository;
_unitRepository = unitRepository;
_categoryRepository = categoryRepository;
_ingredientRepository = ingredientRepository;
Recipes = new List<Recipe>();
_mediaFileRepository = mediaFileRepository;
_instructionRepository = instructionRepository;
_favoriteRepository = favoriteRepository;
_context = context;
}
// GET: /Recipe/{recipeId}/AddOrCreateIngredient
[HttpGet("{recipeId}/AddOrCreateIngredient")]
public async Task<IActionResult> AddOrCreateIngredient(Guid recipeId)
{
if (recipeId == Guid.Empty)
{
return BadRequest("Recipe ID cannot be empty.");
}
var units = await _unitRepository.GetAllUnitsAsync();
var recipeIngredients = await _ingredientRepository.GetIngredientsByRecipeIdAsync(recipeId);
var ingredients = recipeIngredients.Select(ri => ri.Ingredient).ToList();
var viewModel = new IngredientViewModel
{
RecipeId = recipeId,
Ingredients = ingredients,
Units = units.ToList(),
};
return View(viewModel);
}
// GET: /Recipe/Details/{recipeId}
[HttpGet("Details/{recipeId}")]
public async Task<IActionResult> Details(Guid recipeId)
{
var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId);
if (recipe == null)
{
return NotFound("Recipe not found.");
}
return View(recipe);
}
// GET: /Recipe/CategoryRecipes
[HttpGet("CategoryRecipes")]
public async Task<IActionResult> CategoryRecipes()
{
var categoryViewModels = await _categoryRepository.GetAllCategoriesWithRecipesAsync();
return View(categoryViewModels);
}
// POST: /Recipe/{recipeId}/AddOrCreateIngredient
[HttpPost("{recipeId}/AddOrCreateIngredient")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddOrCreateIngredient(Guid recipeId, string ingredientName, int quantity, Guid unitId)
{
if (recipeId == Guid.Empty)
{
return BadRequest("Recipe ID cannot be empty.");
}
if (quantity <= 0)
{
ModelState.AddModelError(nameof(quantity), "Die Menge muss größer als 0 sein.");
}
if (!ModelState.IsValid)
{
var units = await _unitRepository.GetAllUnitsAsync();
ViewBag.Units = new SelectList(units, "Id", "Name");
return View();
}
await _recipeRepository.CreateRecipeIngredientAsync(recipeId, ingredientName, quantity, unitId);
return RedirectToAction("Details", new { id = recipeId });
}
// GET: /Recipe/Create/{categoryId}
[HttpGet("Create/{categoryId}")]
public async Task<IActionResult> Create(Guid categoryId)
{
var category = await _categoryRepository.GetCategoryByIdAsync(categoryId);
if (category == null)
{
return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden.");
}
var units = await _unitRepository.GetAllUnitsAsync();
var viewModel = new CreateRecipeViewModel
{
CategoryId = categoryId,
CategoryName = category.Name,
IngredientViewModel = new IngredientViewModel
{
RecipeId = Guid.Empty,
Ingredients = new List<Ingredient>(),
Units = units.ToList(),
},
InstructionViewModel = new InstructionViewModel
{
RecipeId = Guid.Empty,
Instructions = new List<Instruction>(),
},
};
return View(viewModel);
}
// POST: /Recipe/Create/{categoryId}
[HttpPost("Create/{categoryId}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Guid categoryId, CreateRecipeViewModel model)
{
if (model == null)
{
throw new ArgumentNullException(nameof(model), "CreateRecipeViewModel cannot be null.");
}
if (string.IsNullOrWhiteSpace(model.Name))
{
ModelState.AddModelError(nameof(model.Name), "Name darf nicht leer sein.");
}
if (model.Servings <= 0)
{
ModelState.AddModelError(nameof(model.Servings), "Anzahl der Portionen muss größer als 0 sein.");
}
if (!ModelState.IsValid)
{
var category = await _categoryRepository.GetCategoryByIdAsync(categoryId);
if (category == null)
{
return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden.");
}
var units = await _unitRepository.GetAllUnitsAsync();
if (model.IngredientViewModel == null)
{
model.IngredientViewModel = new IngredientViewModel
{
RecipeId = Guid.Empty,
Ingredients = new List<Ingredient>(),
Units = units.ToList(),
};
}
else
{
model.IngredientViewModel.Units = units.ToList();
}
model.CategoryName = category.Name;
return View(model);
}
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var categoryEntity = await _categoryRepository.GetCategoryByIdAsync(categoryId);
if (categoryEntity == null)
{
return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden.");
}
model.PreparationTime = new TimeSpan(model.PrepHours, model.PrepMinutes, 0);
model.CookingTime = new TimeSpan(model.CookHours, model.CookMinutes, 0);
var recipe = await _recipeRepository.CreateRecipeForCategoryAsync(
categoryEntity,
model.Name,
model.Description ?? string.Empty,
model.Difficulty,
model.Servings,
model.PreparationTime,
model.CookingTime);
if (model.Photo != null)
{
await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, model.Photo);
}
if (model.Video != null)
{
await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, model.Video);
}
if (model.IngredientViewModel?.Ingredients != null)
{
foreach (var ingredient in model.IngredientViewModel.Ingredients)
{
var ri = ingredient.RecipeIngredients?.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(ingredient.Name) && ri?.Quantity > 0 && ri?.Unit?.Id != null)
{
await _recipeRepository.CreateRecipeIngredientAsync(
recipe.Id,
ingredient.Name,
ri.Quantity,
ri.Unit.Id);
}
}
}
if (model.InstructionViewModel?.Instructions != null)
{
for (var i = 0; i < model.InstructionViewModel.Instructions.Count; i++)
{
var instruction = model.InstructionViewModel.Instructions[i];
if (!string.IsNullOrWhiteSpace(instruction.Description))
{
var fileKey = $"InstructionViewModel.Instructions[{i}].MediaFile";
IFormFile? imageFile = null;
if (Request.Form.Files.Any(f => f.Name == fileKey))
{
imageFile = Request.Form.Files[fileKey];
}
await _instructionRepository.CreateInstructionAsync(
recipe.Id,
instruction.Description,
imageFile);
}
}
}
await transaction.CommitAsync();
return RedirectToAction("Details", new { recipeId = recipe.Id });
}
catch (Exception ex)
{
await transaction.RollbackAsync();
return BadRequest(ex.Message);
}
}
// GET: /Recipe/{recipeId}/RemoveIngredient/{ingredientId}
[HttpGet("{recipeId}/RemoveIngredient/{ingredientId}")]
public async Task<IActionResult> RemoveIngredient(Guid recipeId, Guid ingredientId)
{
var recipe = await _recipeRepository.GetRecipeAsync(recipeId);
var ingredient = await _ingredientRepository.GetIngredientByIdAsync(ingredientId);
if (recipe == null || ingredient == null)
{
return NotFound("Recipe or Ingredient not found.");
}
ViewBag.RecipeId = recipeId;
ViewBag.IngredientId = ingredientId;
return View();
}
// DELETE: /Recipe/{recipeId}/RemoveIngredient/{ingredientId}
[HttpDelete("{recipeId}/RemoveIngredient/{ingredientId}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId)
{
await _recipeRepository.RemoveIngredientFromRecipeAsync(recipeId, ingredientId);
TempData["SuccessMessage"] = "Ingredient removed successfully.";
return RedirectToAction("Details", new { id = recipeId });
}
// GET: /Recipe/{recipeId}/RemoveInstruction/{instructionId}
[HttpGet("{recipeId}/RemoveInstruction/{instructionId}")]
public async Task<IActionResult> RemoveInstruction(Guid recipeId, Guid instructionId)
{
var recipe = await _recipeRepository.GetRecipeAsync(recipeId);
if (recipe == null)
{
return NotFound("Recipe not found.");
}
var instruction = recipe.Instructions?.FirstOrDefault(i => i.Id == instructionId);
if (instruction == null)
{
return NotFound("Instruction not found in the specified recipe.");
}
ViewBag.RecipeId = recipeId;
ViewBag.InstructionId = instructionId;
return View();
}
// DELETE: /Recipe/{recipeId}/RemoveInstruction/{instructionId}
[HttpDelete("{recipeId}/RemoveInstruction/{instructionId}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveInstructionConfirmed(Guid recipeId, Guid instructionId)
{
try
{
await _instructionRepository.RemoveInstructionFromRecipeAsync(recipeId, instructionId);
TempData["SuccessMessage"] = "Instruction removed successfully.";
return RedirectToAction("Details", new { recipeId });
}
catch (Exception ex)
{
TempData["ErrorMessage"] = $"An error occurred while removing the instruction: {ex.Message}";
return RedirectToAction("Details", new { recipeId });
}
}
// GET: /Recipe/FilterByDifficulty
[HttpGet("FilterByDifficulty")]
public async Task<IActionResult> FilterByDifficulty(Difficulty? selectedDifficulty)
{
var recipes = await _recipeRepository.GetRecipesByDifficultyAsync(selectedDifficulty);
var viewModel = new FilterByDifficultyViewModel
{
SelectedDifficulty = selectedDifficulty,
Recipes = recipes,
};
return View(viewModel);
}
// GET: /Recipe/{recipeId}/AddInstruction
[HttpGet("{recipeId}/AddInstruction")]
public async Task<IActionResult> AddInstruction(Guid recipeId)
{
var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId);
if (recipe == null)
{
return NotFound("Recipe not found.");
}
var instructions = await _instructionRepository.GetInstructionsOfRecipeAsync(recipeId);
var viewModel = new InstructionViewModel
{
RecipeId = recipeId,
Instructions = instructions,
};
return View(viewModel);
}
// POST: /Recipe/{recipeId}/AddInstruction
[HttpPost("{recipeId}/AddInstruction")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddInstruction(Guid recipeId, string description, IFormFile? image)
{
try
{
await _instructionRepository.CreateInstructionAsync(recipeId, description, image);
var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId);
var instructions = recipe?.Instructions?.ToList() ?? new List<Instruction>();
var viewModel = new InstructionViewModel
{
RecipeId = recipeId,
Instructions = instructions,
};
return RedirectToAction("AddInstruction", new { recipeId });
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
// GET: /Recipe/Favorites
[HttpGet("Favorites")]
public async Task<IActionResult> Favorites()
{
var favoriteRecipes = await _favoriteRepository.GetFavoriteRecipesAsync();
return View(favoriteRecipes);
}
[HttpPost("AddFavorite")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddFavorite(Guid recipeId)
{
await _favoriteRepository.AddFavoriteAsync(recipeId);
// We only need to know the Id and IsFavorite status
var viewModel = new FavoritButtonViewModel
{
Id = recipeId,
IsFavorite = true,
};
return PartialView("_FavoriteButton", viewModel);
}
[HttpPost("RemoveFavorite")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveFavorite(Guid recipeId)
{
await _favoriteRepository.RemoveFavoriteAsync(recipeId);
var viewModel = new FavoritButtonViewModel
{
Id = recipeId,
IsFavorite = false,
};
return PartialView("_FavoriteButton", viewModel);
}
// GET: /Recipe/{recipeId}/GetIngredients
[HttpGet("{recipeId}/GetIngredients")]
public async Task<IActionResult> GetIngredients(Guid recipeId)
{
var recipeIngredients = (await _ingredientRepository.GetIngredientsByRecipeIdAsync(recipeId)).Select(ri => ri.Ingredient).ToList();
var units = await _unitRepository.GetAllUnitsAsync();
var viewModel = new IngredientViewModel
{
RecipeId = recipeId,
Ingredients = recipeIngredients,
Units = units,
};
return PartialView("_IngredientsPartial", viewModel);
}
// 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);
}
[HttpDelete("{recipeId}/Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(Guid recipeId)
{
var deleted = await _recipeRepository.DeleteRecipeAsync(recipeId);
if (deleted)
{
if (Request.Headers.ContainsKey("HX-Request"))
{
Response.Headers["HX-Redirect"] = Url.Action("Index", "Home");
return Ok();
}
return RedirectToAction("Index", "Home");
}
else
{
TempData["ErrorMessage"] = "Rezept nicht gefunden oder konnte nicht gelöscht werden.";
if (Request.Headers.ContainsKey("HX-Request"))
{
Response.Headers["HX-Redirect"] = Url.Action("Details", new { recipeId });
return Ok();
}
return RedirectToAction("Details", new { recipeId });
}
}
}
}
@@ -0,0 +1,192 @@
namespace Francesco.Recipes.World.Controllers
{
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models;
using Francesco.Recipes.World.Repositories.ShoppingList;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
[Route("ShoppingList")]
public class ShoppingListController : Controller
{
private readonly IShoppingListRepository _shoppingListRepository;
private readonly FrancescosRecipesWorldDbContext _context;
public ShoppingListController(IShoppingListRepository shoppingListRepository, FrancescosRecipesWorldDbContext context)
{
_shoppingListRepository = shoppingListRepository;
_context = context;
}
[HttpPost("CreateOrAddIngredients")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> CreateOrAddIngredients([FromBody] CreateOrAddIngredientRequestModel request)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (request == null || request.IngredientIds == null || !request.IngredientIds.Any())
{
return BadRequest("No ingredients provided.");
}
// Compute duplicates for this recipe in the current shopping list
var existingShoppingList = await _context.ShoppingLists
.Include(sl => sl.RecipeShoppingList)
.ThenInclude(rsl => rsl.SelectedIngredients)
.ThenInclude(si => si.RecipeIngredient)
.Include(sl => sl.RecipeShoppingList)
.ThenInclude(rsl => rsl.Recipe)
.FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == request.RecipeId));
var existingForRecipe = existingShoppingList?
.RecipeShoppingList.FirstOrDefault(rsl => rsl.Recipe.Id == request.RecipeId);
var existingIds = existingForRecipe?.SelectedIngredients
.Where(si => si.RecipeIngredient != null)
.Select(si => si.RecipeIngredient.Id)
.ToHashSet() ?? new HashSet<Guid>();
var skippedExisting = request.IngredientIds.Where(id => existingIds.Contains(id)).Distinct().ToList();
var ingredientIdsToAdd = request.IngredientIds.Except(skippedExisting).Distinct().ToList();
if (ingredientIdsToAdd.Count == 0)
{
return Json(new
{
shoppingListId = existingShoppingList?.Id,
skippedExisting,
addedCount = 0,
skippedCount = skippedExisting.Count,
noAdditions = true,
});
}
var shoppingList = await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, ingredientIdsToAdd);
if (shoppingList == null)
{
return BadRequest("Error creating shopping list.");
}
return Json(new
{
shoppingListId = shoppingList.Id,
skippedExisting,
addedCount = ingredientIdsToAdd.Count,
skippedCount = skippedExisting.Count,
});
}
[HttpGet("RecipeCount")]
public async Task<IActionResult> RecipeCount()
{
var count = await _shoppingListRepository.CountAllRecipeShoppinglistsAsync();
return Json(new { count });
}
// GET: /ShoppingList/Details
[HttpGet("Details")]
public async Task<IActionResult> Details()
{
var recipeCount = await _shoppingListRepository.CountAllRecipeShoppinglistsAsync();
var recipeIngredientToShoppingListMap = new Dictionary<Guid, Guid>();
var allEntries = await _context.RecipeIngredientsShoppingLists
.Where(risl => risl.RecipeIngredient != null)
.Select(risl => new
{
RecipeIngredientId = risl.RecipeIngredient.Id,
ShoppingListId = risl.Id,
})
.ToListAsync();
foreach (var entry in allEntries)
{
if (entry.RecipeIngredientId != Guid.Empty &&
!recipeIngredientToShoppingListMap.ContainsKey(entry.RecipeIngredientId))
{
recipeIngredientToShoppingListMap.Add(entry.RecipeIngredientId, entry.ShoppingListId);
}
}
var recipeInAnyShoppingList = (await _shoppingListRepository.GetAllShoppingListsAsync())
.SelectMany(sl => sl.RecipeShoppingList.Select(rsl => rsl.Recipe))
.ToList();
var viewModel = new ShoppingListDetailsViewModel
{
RecipeCount = recipeCount,
RecipesInAnyShoppingList = recipeInAnyShoppingList,
RecipeIngredientToShoppingListMap = recipeIngredientToShoppingListMap,
};
return View(viewModel);
}
[HttpDelete("RemoveIngredients")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveIngredients([FromBody] List<Guid> recipeIngredientShoppingListIds)
{
if (recipeIngredientShoppingListIds == null || !recipeIngredientShoppingListIds.Any())
{
return BadRequest("Keine Zutaten zum Entfernen angegeben.");
}
try
{
var affectedRecipeIds = await _context.RecipeIngredientsShoppingLists
.Where(risl => recipeIngredientShoppingListIds.Contains(risl.Id))
.Select(risl => risl.RecipeShoppingList.Recipe.Id)
.Distinct()
.ToListAsync();
await _shoppingListRepository.RemoveIngredientsFromShoppingListAsync(recipeIngredientShoppingListIds);
var remainingRecipeIds = await _context.RecipeShoppingLists
.Select(rsl => rsl.Recipe.Id)
.ToListAsync();
var removedRecipeIds = affectedRecipeIds
.Where(id => !remainingRecipeIds.Contains(id))
.ToList();
return Json(new { success = true, removedRecipeIds });
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
[HttpDelete("RemoveRecipeFromList/{recipeId}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveRecipeFromList(Guid recipeId)
{
try
{
var recipeShoppingLists = await _context.RecipeShoppingLists
.Where(rsl => rsl.Recipe.Id == recipeId)
.ToListAsync();
if (!recipeShoppingLists.Any())
{
return NotFound($"Kein Einkaufslisten-Eintrag für Rezept mit ID {recipeId} gefunden.");
}
foreach (var recipeShoppingList in recipeShoppingLists)
{
await _shoppingListRepository.RemoveRecipeFromShoppingListAsync(recipeShoppingList.Id);
}
return Ok(new { success = true });
}
catch (Exception ex)
{
return BadRequest(new { success = false, error = ex.Message });
}
}
}
}
@@ -0,0 +1,25 @@
namespace Francesco.Recipes.World.Controller.Unit
{
using Francesco.Recipes.World.Repositories.Unit;
using Microsoft.AspNetCore.Mvc;
[Route("Unit")]
public class UnitController : Controller
{
private readonly IUnitRepository _unitRepository;
public UnitController(IUnitRepository unitRepository)
{
_unitRepository = unitRepository;
}
[HttpGet("GetAllUnits")]
public async Task<IActionResult> GetAllUnits()
{
var units = (await _unitRepository.GetAllUnitsAsync())
.Select(u => new { id = u.Id, name = u.Name })
.ToList();
return Json(units);
}
}
}
@@ -0,0 +1,119 @@
namespace Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models.BackendModels;
using Francesco.Recipes.World.Models.BackendModels.Category;
using Francesco.Recipes.World.Models.BackendModels.Favorit;
using Francesco.Recipes.World.Models.BackendModels.Ingredient;
using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList;
using Francesco.Recipes.World.Models.BackendModels.Instruction;
using Francesco.Recipes.World.Models.BackendModels.MediaFile;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList;
using Francesco.Recipes.World.Models.BackendModels.Shoppinglist;
using Francesco.Recipes.World.Models.BackendModels.Unit;
using Microsoft.EntityFrameworkCore;
public class FrancescosRecipesWorldDbContext : DbContext
{
public FrancescosRecipesWorldDbContext(DbContextOptions<FrancescosRecipesWorldDbContext> options)
: base(options)
{
}
public DbSet<Category> Categories => Set<Category>();
public DbSet<Ingredient> Ingredients => Set<Ingredient>();
public DbSet<Instruction> Instructions => Set<Instruction>();
public DbSet<Recipe> Recipes => Set<Recipe>();
public DbSet<RecipeIngredient> RecipeIngredients => Set<RecipeIngredient>();
public DbSet<Unit> Units => Set<Unit>();
public DbSet<Favorit> Favorits => Set<Favorit>();
public DbSet<RecipeIngredientShoppingList> RecipeIngredientsShoppingLists => Set<RecipeIngredientShoppingList>();
public DbSet<RecipeShoppingList> RecipeShoppingLists => Set<RecipeShoppingList>();
public DbSet<ShoppingList> ShoppingLists => Set<ShoppingList>();
public DbSet<MediaFile> MediaFiles => Set<MediaFile>();
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is ITimeStampedEntity timeStampedEntity)
{
switch (entry.State)
{
case EntityState.Added:
timeStampedEntity.CreatedAt = DateTime.UtcNow;
break;
case EntityState.Modified:
timeStampedEntity.ModifiedAt = DateTime.UtcNow;
break;
}
}
}
return base.SaveChangesAsync(cancellationToken);
}
protected override void OnModelCreating(ModelBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
SeedData(builder);
base.OnModelCreating(builder);
}
private static void SeedData(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>()
.HasData(GetCategories());
modelBuilder.Entity<Unit>()
.HasData(GetUnits());
}
private static IEnumerable<Category> GetCategories()
{
return
[
new Category { Id = Guid.Parse("b248244f-f21c-4555-a14d-5dd49a2717cf"), Name = "Vorspeisen & Snacks" },
new Category { Id = Guid.Parse("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), Name = "Erste Gänge" },
new Category { Id = Guid.Parse("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), Name = "Hauptgerichte" },
new Category { Id = Guid.Parse("90deec39-dcd0-422d-9018-ac8389e332e1"), Name = "Desserts & Süßspeisen" },
new Category { Id = Guid.Parse("28e39168-701a-4084-81da-d96c987c462f"), Name = "Beilagen & Salate" },
new Category { Id = Guid.Parse("20585b74-4805-4aff-a6df-aa6b7af04ff1"), Name = "Kuchen" },
new Category { Id = Guid.Parse("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), Name = "Hefegebäck & Brot" },
new Category { Id = Guid.Parse("d332f88d-d241-48c5-a2f2-bfd124eada7e"), Name = "Soßen & Saucen" },
new Category { Id = Guid.Parse("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), Name = "Marmeladen & Eingemachtes" },
new Category { Id = Guid.Parse("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), Name = "Getränke" },
];
}
private static IEnumerable<Unit> GetUnits()
{
return
[
new Unit { Id = Guid.Parse("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), Name = "liter", Symbol = "l" },
new Unit { Id = Guid.Parse("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), Name = "gramm", Symbol = "g" },
new Unit { Id = Guid.Parse("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), Name = "kilogramm", Symbol = "kg" },
new Unit { Id = Guid.Parse("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), Name = "stücke", Symbol = "stk" },
new Unit { Id = Guid.Parse("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), Name = "blatt", Symbol = "blatt" },
new Unit { Id = Guid.Parse("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), Name = "messerspitze", Symbol = "msp" },
new Unit { Id = Guid.Parse("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), Name = "stange", Symbol = "stange" },
new Unit { Id = Guid.Parse("7ea2f51d-7493-4f19-a663-1f309186d3ae"), Name = "bund", Symbol = "bund" },
new Unit { Id = Guid.Parse("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), Name = "zehe", Symbol = "zehe" },
new Unit { Id = Guid.Parse("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), Name = "teelöffel", Symbol = "TL" },
new Unit { Id = Guid.Parse("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), Name = "esslöffel", Symbol = "EL" },
];
}
}
@@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>be6a70eb-b657-40b2-b4a1-418e5c6ec131</UserSecretsId>
</PropertyGroup>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisRuleSet>$(SolutionDir)Francesco.Recipes.World.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.11" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
<PackageReference Include="SecurityCodeScan.VS2019" Version="5.6.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Sentry.Serilog" Version="4.13.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include=".\stylecop.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\Category\" />
<Folder Include="Services\MediaFile\" />
<Folder Include="Services\Ingredient\" />
</ItemGroup>
</Project>
@@ -0,0 +1,513 @@
// <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("20250318150949_InitialMIgration")]
partial class InitialMIgration
{
/// <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.IngredientsShoppingList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("IngredientId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ShoppinglistId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("IngredientId");
b.HasIndex("ShoppinglistId");
b.ToTable("IngredientsShoppingLists");
});
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<string>("Number")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RecipeId");
b.ToTable("Instructions");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<byte[]>("Data")
.HasColumnType("varbinary(max)");
b.Property<string>("FileName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("InstructionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("MimeType")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("RecipeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("InstructionId");
b.HasIndex("RecipeId");
b.ToTable("MediaFiles");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<TimeSpan>("CookingTime")
.HasColumnType("time");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<int>("Difficulty")
.HasColumnType("int");
b.Property<Guid>("FavoritId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsFavorite")
.HasColumnType("bit");
b.Property<DateTime?>("ModifiedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<TimeSpan>("PreparationTime")
.HasColumnType("time");
b.Property<int>("Servings")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("FavoritId");
b.ToTable("Recipes");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("IngredientId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("UnitId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("IngredientId");
b.HasIndex("RecipeId");
b.HasIndex("UnitId");
b.ToTable("RecipeIngredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.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.IngredientsShoppingList", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient")
.WithMany("IngredientShoppingLists")
.HasForeignKey("IngredientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "Shoppinglist")
.WithMany("IngredientsShoppingLists")
.HasForeignKey("ShoppinglistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ingredient");
b.Navigation("Shoppinglist");
});
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")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
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", null)
.WithMany("Recipes")
.HasForeignKey("CategoryId");
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit")
.WithMany("Recipe")
.HasForeignKey("FavoritId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Favorit");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient")
.WithMany("RecipeIngredients")
.HasForeignKey("IngredientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany("RecipeIngredients")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit")
.WithMany("RecipeIngredient")
.HasForeignKey("UnitId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ingredient");
b.Navigation("Recipe");
b.Navigation("Unit");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.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.Shoppinglist.ShoppingList", b =>
{
b.Navigation("IngredientsShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b =>
{
b.Navigation("RecipeIngredient");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,346 @@
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Francesco.Recipes.World.Migrations
{
using System;
using Microsoft.EntityFrameworkCore.Migrations;
/// <inheritdoc />
public partial class InitialMIgration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder is null)
{
throw new ArgumentNullException(nameof(migrationBuilder));
}
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Favorits",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_Favorits", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Ingredients",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_Ingredients", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ShoppingLists",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_ShoppingLists", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Units",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Symbol = table.Column<string>(type: "nvarchar(max)", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_Units", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Recipes",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
Difficulty = table.Column<int>(type: "int", nullable: false),
Servings = table.Column<int>(type: "int", nullable: false),
PreparationTime = table.Column<TimeSpan>(type: "time", nullable: false),
CookingTime = table.Column<TimeSpan>(type: "time", nullable: false),
IsFavorite = table.Column<bool>(type: "bit", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
FavoritId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_Recipes", x => x.Id);
table.ForeignKey(
name: "FK_Recipes_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Recipes_Favorits_FavoritId",
column: x => x.FavoritId,
principalTable: "Favorits",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "IngredientsShoppingLists",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ShoppinglistId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
IngredientId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_IngredientsShoppingLists", x => x.Id);
table.ForeignKey(
name: "FK_IngredientsShoppingLists_Ingredients_IngredientId",
column: x => x.IngredientId,
principalTable: "Ingredients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_IngredientsShoppingLists_ShoppingLists_ShoppinglistId",
column: x => x.ShoppinglistId,
principalTable: "ShoppingLists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Instructions",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Description = table.Column<string>(type: "nvarchar(max)", nullable: false),
Number = table.Column<string>(type: "nvarchar(max)", nullable: false),
RecipeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_Instructions", x => x.Id);
table.ForeignKey(
name: "FK_Instructions_Recipes_RecipeId",
column: x => x.RecipeId,
principalTable: "Recipes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "RecipeIngredients",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
RecipeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
IngredientId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
UnitId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_RecipeIngredients", x => x.Id);
table.ForeignKey(
name: "FK_RecipeIngredients_Ingredients_IngredientId",
column: x => x.IngredientId,
principalTable: "Ingredients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RecipeIngredients_Recipes_RecipeId",
column: x => x.RecipeId,
principalTable: "Recipes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RecipeIngredients_Units_UnitId",
column: x => x.UnitId,
principalTable: "Units",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MediaFiles",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
FileName = table.Column<string>(type: "nvarchar(max)", nullable: true),
MimeType = table.Column<string>(type: "nvarchar(max)", nullable: true),
Data = table.Column<byte[]>(type: "varbinary(max)", nullable: true),
RecipeId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
InstructionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_MediaFiles", x => x.Id);
table.ForeignKey(
name: "FK_MediaFiles_Instructions_InstructionId",
column: x => x.InstructionId,
principalTable: "Instructions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MediaFiles_Recipes_RecipeId",
column: x => x.RecipeId,
principalTable: "Recipes",
principalColumn: "Id");
});
migrationBuilder.InsertData(
table: "Categories",
columns: new[] { "Id", "Name" },
values: new object[,]
{
{ new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), "Getränke" },
{ new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"), "Kuchen" },
{ new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), "Marmeladen & Eingemachtes" },
{ new Guid("28e39168-701a-4084-81da-d96c987c462f"), "Beilagen & Salate" },
{ new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), "Erste Gänge" },
{ new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"), "Desserts & Süßspeisen" },
{ new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), "Hauptgerichte" },
{ new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), "Hefegebäck & Brot" },
{ new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"), "Vorspeisen & Snacks" },
{ new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"), "Soßen & Saucen" },
});
migrationBuilder.InsertData(
table: "Units",
columns: new[] { "Id", "Name", "Symbol" },
values: new object[,]
{
{ new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), "stücke", "stk" },
{ new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), "esslöffel", "EL" },
{ new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), "teelöffel", "TL" },
{ new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), "liter", "l" },
{ new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), "zehe", "zehe" },
{ new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), "gramm", "g" },
{ new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), "kilogramm", "kg" },
{ new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"), "bund", "bund" },
{ new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), "blatt", "blatt" },
{ new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), "messerspitze", "msp" },
{ new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), "stange", "stange" },
});
migrationBuilder.CreateIndex(
name: "IX_IngredientsShoppingLists_IngredientId",
table: "IngredientsShoppingLists",
column: "IngredientId");
migrationBuilder.CreateIndex(
name: "IX_IngredientsShoppingLists_ShoppinglistId",
table: "IngredientsShoppingLists",
column: "ShoppinglistId");
migrationBuilder.CreateIndex(
name: "IX_Instructions_RecipeId",
table: "Instructions",
column: "RecipeId");
migrationBuilder.CreateIndex(
name: "IX_MediaFiles_InstructionId",
table: "MediaFiles",
column: "InstructionId");
migrationBuilder.CreateIndex(
name: "IX_MediaFiles_RecipeId",
table: "MediaFiles",
column: "RecipeId");
migrationBuilder.CreateIndex(
name: "IX_RecipeIngredients_IngredientId",
table: "RecipeIngredients",
column: "IngredientId");
migrationBuilder.CreateIndex(
name: "IX_RecipeIngredients_RecipeId",
table: "RecipeIngredients",
column: "RecipeId");
migrationBuilder.CreateIndex(
name: "IX_RecipeIngredients_UnitId",
table: "RecipeIngredients",
column: "UnitId");
migrationBuilder.CreateIndex(
name: "IX_Recipes_CategoryId",
table: "Recipes",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Recipes_FavoritId",
table: "Recipes",
column: "FavoritId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
if (migrationBuilder is null)
{
throw new ArgumentNullException(nameof(migrationBuilder));
}
migrationBuilder.DropTable(
name: "IngredientsShoppingLists");
migrationBuilder.DropTable(
name: "MediaFiles");
migrationBuilder.DropTable(
name: "RecipeIngredients");
migrationBuilder.DropTable(
name: "ShoppingLists");
migrationBuilder.DropTable(
name: "Instructions");
migrationBuilder.DropTable(
name: "Ingredients");
migrationBuilder.DropTable(
name: "Units");
migrationBuilder.DropTable(
name: "Recipes");
migrationBuilder.DropTable(
name: "Categories");
migrationBuilder.DropTable(
name: "Favorits");
}
}
}
@@ -0,0 +1,518 @@
// <auto-generated />
using Francesco.Recipes.World.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Francesco.Recipes.World.Migrations
{
[DbContext(typeof(FrancescosRecipesWorldDbContext))]
[Migration("20250324124459_CreateNewTableRecipeIngredientShoppinglist")]
partial class CreateNewTableRecipeIngredientShoppinglist
{
/// <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<Guid>("RecipeIngredientId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ShoppingListId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("IngredientId");
b.HasIndex("RecipeIngredientId");
b.HasIndex("ShoppingListId");
b.ToTable("RecipeIngredientsShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Number")
.HasColumnType("int");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RecipeId");
b.ToTable("Instructions");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<byte[]>("Data")
.HasColumnType("varbinary(max)");
b.Property<string>("FileName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("InstructionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("MimeType")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("RecipeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("InstructionId");
b.HasIndex("RecipeId");
b.ToTable("MediaFiles");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<TimeSpan>("CookingTime")
.HasColumnType("time");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<int>("Difficulty")
.HasColumnType("int");
b.Property<Guid>("FavoritId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsFavorite")
.HasColumnType("bit");
b.Property<DateTime?>("ModifiedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<TimeSpan>("PreparationTime")
.HasColumnType("time");
b.Property<int>("Servings")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("FavoritId");
b.ToTable("Recipes");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("IngredientId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("UnitId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("IngredientId");
b.HasIndex("RecipeId");
b.HasIndex("UnitId");
b.ToTable("RecipeIngredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.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.Shoppinglist.ShoppingList", "ShoppingList")
.WithMany("RecipeIngredientShoppingLists")
.HasForeignKey("ShoppingListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RecipeIngredient");
b.Navigation("ShoppingList");
});
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")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
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", null)
.WithMany("Recipes")
.HasForeignKey("CategoryId");
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit")
.WithMany("Recipe")
.HasForeignKey("FavoritId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Favorit");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient")
.WithMany("RecipeIngredients")
.HasForeignKey("IngredientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany("RecipeIngredients")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit")
.WithMany("RecipeIngredient")
.HasForeignKey("UnitId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ingredient");
b.Navigation("Recipe");
b.Navigation("Unit");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.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.Shoppinglist.ShoppingList", b =>
{
b.Navigation("RecipeIngredientShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b =>
{
b.Navigation("RecipeIngredient");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,131 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Francesco.Recipes.World.Migrations
{
/// <inheritdoc />
public partial class CreateNewTableRecipeIngredientShoppinglist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder is null)
{
throw new ArgumentNullException(nameof(migrationBuilder));
}
migrationBuilder.DropTable(
name: "IngredientsShoppingLists");
migrationBuilder.AlterColumn<int>(
name: "Number",
table: "Instructions",
type: "int",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
migrationBuilder.CreateTable(
name: "RecipeIngredientsShoppingLists",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ShoppingListId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
RecipeIngredientId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
IngredientId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_RecipeIngredientsShoppingLists", x => x.Id);
table.ForeignKey(
name: "FK_RecipeIngredientsShoppingLists_Ingredients_IngredientId",
column: x => x.IngredientId,
principalTable: "Ingredients",
principalColumn: "Id");
table.ForeignKey(
name: "FK_RecipeIngredientsShoppingLists_RecipeIngredients_RecipeIngredientId",
column: x => x.RecipeIngredientId,
principalTable: "RecipeIngredients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId",
column: x => x.ShoppingListId,
principalTable: "ShoppingLists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_RecipeIngredientsShoppingLists_IngredientId",
table: "RecipeIngredientsShoppingLists",
column: "IngredientId");
migrationBuilder.CreateIndex(
name: "IX_RecipeIngredientsShoppingLists_RecipeIngredientId",
table: "RecipeIngredientsShoppingLists",
column: "RecipeIngredientId");
migrationBuilder.CreateIndex(
name: "IX_RecipeIngredientsShoppingLists_ShoppingListId",
table: "RecipeIngredientsShoppingLists",
column: "ShoppingListId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
if (migrationBuilder is null)
{
throw new ArgumentNullException(nameof(migrationBuilder));
}
migrationBuilder.DropTable(
name: "RecipeIngredientsShoppingLists");
migrationBuilder.AlterColumn<string>(
name: "Number",
table: "Instructions",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.CreateTable(
name: "IngredientsShoppingLists",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
IngredientId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ShoppinglistId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_IngredientsShoppingLists", x => x.Id);
table.ForeignKey(
name: "FK_IngredientsShoppingLists_Ingredients_IngredientId",
column: x => x.IngredientId,
principalTable: "Ingredients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_IngredientsShoppingLists_ShoppingLists_ShoppinglistId",
column: x => x.ShoppinglistId,
principalTable: "ShoppingLists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_IngredientsShoppingLists_IngredientId",
table: "IngredientsShoppingLists",
column: "IngredientId");
migrationBuilder.CreateIndex(
name: "IX_IngredientsShoppingLists_ShoppinglistId",
table: "IngredientsShoppingLists",
column: "ShoppinglistId");
}
}
}
@@ -0,0 +1,572 @@
// <auto-generated />
using Francesco.Recipes.World.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Francesco.Recipes.World.Migrations
{
[DbContext(typeof(FrancescosRecipesWorldDbContext))]
[Migration("20250402120842_UpdateShoppingLIstLogic")]
partial class UpdateShoppingLIstLogic
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Categories");
b.HasData(
new
{
Id = new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"),
Name = "Vorspeisen & Snacks"
},
new
{
Id = new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"),
Name = "Erste Gänge"
},
new
{
Id = new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"),
Name = "Hauptgerichte"
},
new
{
Id = new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"),
Name = "Desserts & Süßspeisen"
},
new
{
Id = new Guid("28e39168-701a-4084-81da-d96c987c462f"),
Name = "Beilagen & Salate"
},
new
{
Id = new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"),
Name = "Kuchen"
},
new
{
Id = new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"),
Name = "Hefegebäck & Brot"
},
new
{
Id = new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"),
Name = "Soßen & Saucen"
},
new
{
Id = new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"),
Name = "Marmeladen & Eingemachtes"
},
new
{
Id = new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"),
Name = "Getränke"
});
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Favorits");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Ingredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("IngredientId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsChecked")
.HasColumnType("bit");
b.Property<Guid>("RecipeIngredientId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RecipeShoppingListId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("IngredientId");
b.HasIndex("RecipeIngredientId");
b.HasIndex("RecipeShoppingListId");
b.ToTable("RecipeIngredientsShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Number")
.HasColumnType("int");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RecipeId");
b.ToTable("Instructions");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<byte[]>("Data")
.HasColumnType("varbinary(max)");
b.Property<string>("FileName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("InstructionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("MimeType")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("RecipeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("InstructionId");
b.HasIndex("RecipeId");
b.ToTable("MediaFiles");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<TimeSpan>("CookingTime")
.HasColumnType("time");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<int>("Difficulty")
.HasColumnType("int");
b.Property<Guid>("FavoritId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsFavorite")
.HasColumnType("bit");
b.Property<DateTime?>("ModifiedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<TimeSpan>("PreparationTime")
.HasColumnType("time");
b.Property<int>("Servings")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("FavoritId");
b.ToTable("Recipes");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("IngredientId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("UnitId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("IngredientId");
b.HasIndex("RecipeId");
b.HasIndex("UnitId");
b.ToTable("RecipeIngredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ShoppingListId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RecipeId");
b.HasIndex("ShoppingListId");
b.ToTable("RecipeShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<DateTime?>("ModifiedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("ShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Symbol")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Units");
b.HasData(
new
{
Id = new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"),
Name = "liter",
Symbol = "l"
},
new
{
Id = new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"),
Name = "gramm",
Symbol = "g"
},
new
{
Id = new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"),
Name = "kilogramm",
Symbol = "kg"
},
new
{
Id = new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"),
Name = "stücke",
Symbol = "stk"
},
new
{
Id = new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"),
Name = "blatt",
Symbol = "blatt"
},
new
{
Id = new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"),
Name = "messerspitze",
Symbol = "msp"
},
new
{
Id = new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"),
Name = "stange",
Symbol = "stange"
},
new
{
Id = new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"),
Name = "bund",
Symbol = "bund"
},
new
{
Id = new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"),
Name = "zehe",
Symbol = "zehe"
},
new
{
Id = new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"),
Name = "teelöffel",
Symbol = "TL"
},
new
{
Id = new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"),
Name = "esslöffel",
Symbol = "EL"
});
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null)
.WithMany("IngredientShoppingLists")
.HasForeignKey("IngredientId");
b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient")
.WithMany()
.HasForeignKey("RecipeIngredientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList")
.WithMany("SelectedIngredients")
.HasForeignKey("RecipeShoppingListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RecipeIngredient");
b.Navigation("RecipeShoppingList");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany("Instructions")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Recipe");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction")
.WithMany("MediaFiles")
.HasForeignKey("InstructionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany("MediaFiles")
.HasForeignKey("RecipeId");
b.Navigation("Instruction");
b.Navigation("Recipe");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category")
.WithMany("Recipes")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit")
.WithMany("Recipe")
.HasForeignKey("FavoritId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
b.Navigation("Favorit");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient")
.WithMany("RecipeIngredients")
.HasForeignKey("IngredientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany("RecipeIngredients")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit")
.WithMany("RecipeIngredient")
.HasForeignKey("UnitId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ingredient");
b.Navigation("Recipe");
b.Navigation("Unit");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany()
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList")
.WithMany("RecipeIngredientShoppingLists")
.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("RecipeIngredientShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b =>
{
b.Navigation("RecipeIngredient");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,162 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Francesco.Recipes.World.Migrations
{
/// <inheritdoc />
public partial class UpdateShoppingLIstLogic : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder is null)
{
throw new ArgumentNullException(nameof(migrationBuilder));
}
migrationBuilder.DropForeignKey(
name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId",
table: "RecipeIngredientsShoppingLists");
migrationBuilder.DropForeignKey(
name: "FK_Recipes_Categories_CategoryId",
table: "Recipes");
migrationBuilder.RenameColumn(
name: "ShoppingListId",
table: "RecipeIngredientsShoppingLists",
newName: "RecipeShoppingListId");
migrationBuilder.RenameIndex(
name: "IX_RecipeIngredientsShoppingLists_ShoppingListId",
table: "RecipeIngredientsShoppingLists",
newName: "IX_RecipeIngredientsShoppingLists_RecipeShoppingListId");
migrationBuilder.AlterColumn<Guid>(
name: "CategoryId",
table: "Recipes",
type: "uniqueidentifier",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "uniqueidentifier",
oldNullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsChecked",
table: "RecipeIngredientsShoppingLists",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "RecipeShoppingLists",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ShoppingListId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
RecipeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_RecipeShoppingLists", x => x.Id);
table.ForeignKey(
name: "FK_RecipeShoppingLists_Recipes_RecipeId",
column: x => x.RecipeId,
principalTable: "Recipes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RecipeShoppingLists_ShoppingLists_ShoppingListId",
column: x => x.ShoppingListId,
principalTable: "ShoppingLists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_RecipeShoppingLists_RecipeId",
table: "RecipeShoppingLists",
column: "RecipeId");
migrationBuilder.CreateIndex(
name: "IX_RecipeShoppingLists_ShoppingListId",
table: "RecipeShoppingLists",
column: "ShoppingListId");
migrationBuilder.AddForeignKey(
name: "FK_RecipeIngredientsShoppingLists_RecipeShoppingLists_RecipeShoppingListId",
table: "RecipeIngredientsShoppingLists",
column: "RecipeShoppingListId",
principalTable: "RecipeShoppingLists",
principalColumn: "Id",
onDelete: ReferentialAction.NoAction);
migrationBuilder.AddForeignKey(
name: "FK_Recipes_Categories_CategoryId",
table: "Recipes",
column: "CategoryId",
principalTable: "Categories",
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_RecipeIngredientsShoppingLists_RecipeShoppingLists_RecipeShoppingListId",
table: "RecipeIngredientsShoppingLists");
migrationBuilder.DropForeignKey(
name: "FK_Recipes_Categories_CategoryId",
table: "Recipes");
migrationBuilder.DropTable(
name: "RecipeShoppingLists");
migrationBuilder.DropColumn(
name: "IsChecked",
table: "RecipeIngredientsShoppingLists");
migrationBuilder.RenameColumn(
name: "RecipeShoppingListId",
table: "RecipeIngredientsShoppingLists",
newName: "ShoppingListId");
migrationBuilder.RenameIndex(
name: "IX_RecipeIngredientsShoppingLists_RecipeShoppingListId",
table: "RecipeIngredientsShoppingLists",
newName: "IX_RecipeIngredientsShoppingLists_ShoppingListId");
migrationBuilder.AlterColumn<Guid>(
name: "CategoryId",
table: "Recipes",
type: "uniqueidentifier",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uniqueidentifier");
migrationBuilder.AddForeignKey(
name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId",
table: "RecipeIngredientsShoppingLists",
column: "ShoppingListId",
principalTable: "ShoppingLists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Recipes_Categories_CategoryId",
table: "Recipes",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "Id");
}
}
}
@@ -0,0 +1,571 @@
// <auto-generated />
using System;
using Francesco.Recipes.World.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Francesco.Recipes.World.Migrations
{
[DbContext(typeof(FrancescosRecipesWorldDbContext))]
[Migration("20250430094852_MakeInstructionOptionalInMediaFile")]
partial class MakeInstructionOptionalInMediaFile
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Categories");
b.HasData(
new
{
Id = new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"),
Name = "Vorspeisen & Snacks"
},
new
{
Id = new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"),
Name = "Erste Gänge"
},
new
{
Id = new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"),
Name = "Hauptgerichte"
},
new
{
Id = new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"),
Name = "Desserts & Süßspeisen"
},
new
{
Id = new Guid("28e39168-701a-4084-81da-d96c987c462f"),
Name = "Beilagen & Salate"
},
new
{
Id = new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"),
Name = "Kuchen"
},
new
{
Id = new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"),
Name = "Hefegebäck & Brot"
},
new
{
Id = new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"),
Name = "Soßen & Saucen"
},
new
{
Id = new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"),
Name = "Marmeladen & Eingemachtes"
},
new
{
Id = new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"),
Name = "Getränke"
});
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Favorits");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Ingredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("IngredientId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsChecked")
.HasColumnType("bit");
b.Property<Guid>("RecipeIngredientId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RecipeShoppingListId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("IngredientId");
b.HasIndex("RecipeIngredientId");
b.HasIndex("RecipeShoppingListId");
b.ToTable("RecipeIngredientsShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Number")
.HasColumnType("int");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RecipeId");
b.ToTable("Instructions");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<byte[]>("Data")
.HasColumnType("varbinary(max)");
b.Property<string>("FileName")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("InstructionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("MimeType")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("RecipeId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("InstructionId");
b.HasIndex("RecipeId");
b.ToTable("MediaFiles");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<TimeSpan>("CookingTime")
.HasColumnType("time");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<int>("Difficulty")
.HasColumnType("int");
b.Property<Guid>("FavoritId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsFavorite")
.HasColumnType("bit");
b.Property<DateTime?>("ModifiedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<TimeSpan>("PreparationTime")
.HasColumnType("time");
b.Property<int>("Servings")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("FavoritId");
b.ToTable("Recipes");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("IngredientId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("UnitId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("IngredientId");
b.HasIndex("RecipeId");
b.HasIndex("UnitId");
b.ToTable("RecipeIngredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RecipeId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ShoppingListId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RecipeId");
b.HasIndex("ShoppingListId");
b.ToTable("RecipeShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<DateTime?>("ModifiedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("ShoppingLists");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Symbol")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Units");
b.HasData(
new
{
Id = new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"),
Name = "liter",
Symbol = "l"
},
new
{
Id = new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"),
Name = "gramm",
Symbol = "g"
},
new
{
Id = new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"),
Name = "kilogramm",
Symbol = "kg"
},
new
{
Id = new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"),
Name = "stücke",
Symbol = "stk"
},
new
{
Id = new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"),
Name = "blatt",
Symbol = "blatt"
},
new
{
Id = new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"),
Name = "messerspitze",
Symbol = "msp"
},
new
{
Id = new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"),
Name = "stange",
Symbol = "stange"
},
new
{
Id = new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"),
Name = "bund",
Symbol = "bund"
},
new
{
Id = new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"),
Name = "zehe",
Symbol = "zehe"
},
new
{
Id = new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"),
Name = "teelöffel",
Symbol = "TL"
},
new
{
Id = new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"),
Name = "esslöffel",
Symbol = "EL"
});
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null)
.WithMany("IngredientShoppingLists")
.HasForeignKey("IngredientId");
b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient")
.WithMany()
.HasForeignKey("RecipeIngredientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList")
.WithMany("SelectedIngredients")
.HasForeignKey("RecipeShoppingListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RecipeIngredient");
b.Navigation("RecipeShoppingList");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany("Instructions")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Recipe");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction")
.WithMany("MediaFiles")
.HasForeignKey("InstructionId");
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany("MediaFiles")
.HasForeignKey("RecipeId");
b.Navigation("Instruction");
b.Navigation("Recipe");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category")
.WithMany("Recipes")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit")
.WithMany("Recipe")
.HasForeignKey("FavoritId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
b.Navigation("Favorit");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient")
.WithMany("RecipeIngredients")
.HasForeignKey("IngredientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany("RecipeIngredients")
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit")
.WithMany("RecipeIngredient")
.HasForeignKey("UnitId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Ingredient");
b.Navigation("Recipe");
b.Navigation("Unit");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b =>
{
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe")
.WithMany()
.HasForeignKey("RecipeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList")
.WithMany("RecipeShoppingList")
.HasForeignKey("ShoppingListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Recipe");
b.Navigation("ShoppingList");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b =>
{
b.Navigation("Recipes");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b =>
{
b.Navigation("Recipe");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b =>
{
b.Navigation("IngredientShoppingLists");
b.Navigation("RecipeIngredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b =>
{
b.Navigation("MediaFiles");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b =>
{
b.Navigation("Instructions");
b.Navigation("MediaFiles");
b.Navigation("RecipeIngredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b =>
{
b.Navigation("SelectedIngredients");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b =>
{
b.Navigation("RecipeShoppingList");
});
modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b =>
{
b.Navigation("RecipeIngredient");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,69 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Francesco.Recipes.World.Migrations
{
/// <inheritdoc />
public partial class MakeInstructionOptionalInMediaFile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder is null)
{
throw new ArgumentNullException(nameof(migrationBuilder));
}
migrationBuilder.DropForeignKey(
name: "FK_MediaFiles_Instructions_InstructionId",
table: "MediaFiles");
migrationBuilder.AlterColumn<Guid>(
name: "InstructionId",
table: "MediaFiles",
type: "uniqueidentifier",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uniqueidentifier");
migrationBuilder.AddForeignKey(
name: "FK_MediaFiles_Instructions_InstructionId",
table: "MediaFiles",
column: "InstructionId",
principalTable: "Instructions",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
if (migrationBuilder is null)
{
throw new ArgumentNullException(nameof(migrationBuilder));
}
migrationBuilder.DropForeignKey(
name: "FK_MediaFiles_Instructions_InstructionId",
table: "MediaFiles");
migrationBuilder.AlterColumn<Guid>(
name: "InstructionId",
table: "MediaFiles",
type: "uniqueidentifier",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "uniqueidentifier",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_MediaFiles_Instructions_InstructionId",
table: "MediaFiles",
column: "InstructionId",
principalTable: "Instructions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}
@@ -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);
}
}
}
@@ -0,0 +1,568 @@
// <auto-generated />
using System;
using Francesco.Recipes.World.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Francesco.Recipes.World.Migrations
{
[DbContext(typeof(FrancescosRecipesWorldDbContext))]
partial class FrancescosRecipesWorldDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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,13 @@
namespace Francesco.Recipes.World.Models.BackendModels.Category
{
using Francesco.Recipes.World.Models.BackendModels.Recipe;
public class Category
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public virtual ICollection<Recipe> Recipes { get; set; } = new List<Recipe>();
}
}
@@ -0,0 +1,13 @@
namespace Francesco.Recipes.World.Models.BackendModels.Favorit
{
using Francesco.Recipes.World.Models.BackendModels.Recipe;
public class Favorit
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public virtual ICollection<Recipe> Recipe { get; set; } = new List<Recipe>();
}
}
@@ -0,0 +1,9 @@
namespace Francesco.Recipes.World.Models.BackendModels
{
public interface ITimeStampedEntity
{
DateTime CreatedAt { get; set; }
DateTime? ModifiedAt { get; set; }
}
}
@@ -0,0 +1,16 @@
namespace Francesco.Recipes.World.Models.BackendModels.Ingredient
{
using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList;
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
public class Ingredient
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public virtual ICollection<RecipeIngredient> RecipeIngredients { get; set; } = new List<RecipeIngredient>();
public virtual ICollection<RecipeIngredientShoppingList> IngredientShoppingLists { get; set; } = new List<RecipeIngredientShoppingList>();
}
}
@@ -0,0 +1,18 @@
namespace Francesco.Recipes.World.Models.BackendModels.Instruction
{
using Francesco.Recipes.World.Models.BackendModels.MediaFile;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
public class Instruction
{
public Guid Id { get; set; }
public string Description { get; set; } = string.Empty;
public int Number { get; set; }
public virtual Recipe Recipe { get; set; } = new ();
public virtual ICollection<MediaFile> MediaFiles { get; set; } = new List<MediaFile>();
}
}
@@ -0,0 +1,20 @@
namespace Francesco.Recipes.World.Models.BackendModels.MediaFile
{
using Francesco.Recipes.World.Models.BackendModels.Instruction;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
public class MediaFile
{
public Guid Id { get; set; } = Guid.NewGuid();
public string? FileName { get; set; }
public string? MimeType { get; set; }
public byte[]? Data { get; set; }
public virtual Recipe? Recipe { get; set; } = null;
public virtual Instruction? Instruction { get; set; } = null;
}
}
@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Francesco.Recipes.World.Models.BackendModels.Recipe
{
public enum Difficulty
{
[Display(Name = "Sehr einfach")]
VeryEasy = 0,
[Display(Name = "Einfach")]
Easy = 1,
[Display(Name = "Mittel")]
Medium = 2,
[Display(Name = "Schwer")]
Hard = 3,
[Display(Name = "Experte")]
Expert = 4,
}
}
@@ -0,0 +1,40 @@
namespace Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Models.BackendModels.Category;
using Francesco.Recipes.World.Models.BackendModels.Favorit;
using Francesco.Recipes.World.Models.BackendModels.Instruction;
using Francesco.Recipes.World.Models.BackendModels.MediaFile;
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
public class Recipe : ITimeStampedEntity, IFavoritable
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public Difficulty Difficulty { get; set; }
public int Servings { get; set; }
public TimeSpan PreparationTime { get; set; }
public TimeSpan CookingTime { get; set; }
public bool IsFavorite { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public virtual ICollection<RecipeIngredient> RecipeIngredients { get; set; } = new List<RecipeIngredient>();
public virtual ICollection<Instruction> Instructions { get; set; } = new List<Instruction>();
public virtual ICollection<MediaFile> MediaFiles { get; set; } = new List<MediaFile>();
public virtual Favorit Favorite { get; set; } = new ();
public virtual Category Category { get; set; } = new ();
}
@@ -0,0 +1,19 @@
namespace Francesco.Recipes.World.Models.BackendModels.RecipeIngredient
{
using Francesco.Recipes.World.Models.BackendModels.Ingredient;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Models.BackendModels.Unit;
public class RecipeIngredient
{
public Guid Id { get; set; }
public virtual Recipe Recipe { get; set; } = new ();
public virtual Ingredient Ingredient { get; set; } = new ();
public virtual Unit Unit { get; set; } = new ();
public int Quantity { get; set; }
}
}
@@ -0,0 +1,16 @@
namespace Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList
{
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList;
public class RecipeIngredientShoppingList
{
public Guid Id { get; set; }
public virtual RecipeShoppingList RecipeShoppingList { get; set; } = new ();
public virtual RecipeIngredient RecipeIngredient { get; set; } = new ();
public bool IsChecked { get; set; } = false;
}
}
@@ -0,0 +1,17 @@
namespace Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList
{
using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Models.BackendModels.Shoppinglist;
public class RecipeShoppingList
{
public Guid Id { get; set; }
public virtual ShoppingList ShoppingList { get; set; } = new ShoppingList();
public virtual Recipe Recipe { get; set; } = new Recipe();
public virtual ICollection<RecipeIngredientShoppingList> SelectedIngredients { get; set; } = new List<RecipeIngredientShoppingList>();
}
}
@@ -0,0 +1,15 @@
namespace Francesco.Recipes.World.Models.BackendModels.Shoppinglist
{
using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList;
public class ShoppingList : ITimeStampedEntity
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public virtual ICollection<RecipeShoppingList> RecipeShoppingList { get; set; } = new List<RecipeShoppingList>();
}
}
@@ -0,0 +1,15 @@
namespace Francesco.Recipes.World.Models.BackendModels.Unit
{
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
public class Unit
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public virtual ICollection<RecipeIngredient> RecipeIngredient { get; set; } = new List<RecipeIngredient>();
}
}
@@ -0,0 +1,9 @@
namespace Francesco.Recipes.World.Models
{
public class CreateOrAddIngredientRequestModel
{
public Guid RecipeId { get; set; }
public List<Guid> IngredientIds { get; set; } = new ();
}
}
@@ -0,0 +1,39 @@
using Francesco.Recipes.World.Models.BackendModels.Recipe;
namespace Francesco.Recipes.World.Models
{
public class CreateRecipeViewModel
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public Difficulty Difficulty { get; set; }
public int Servings { get; set; }
public TimeSpan PreparationTime { get; set; }
public TimeSpan CookingTime { get; set; }
public int PrepHours { get; set; }
public int PrepMinutes { get; set; }
public int CookHours { get; set; }
public int CookMinutes { get; set; }
public Guid CategoryId { get; set; }
public string? CategoryName { get; set; }
public IFormFile? Photo { get; set; }
public IFormFile? Video { get; set; }
public IngredientViewModel? IngredientViewModel { get; set; }
public InstructionViewModel? InstructionViewModel { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace Francesco.Recipes.World.Models
{
public class ErrorViewModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}
@@ -0,0 +1,9 @@
namespace Francesco.Recipes.World.Models
{
public class FavoritButtonViewModel : IFavoritable
{
public Guid Id { get; set; }
public bool IsFavorite { get; set; }
}
}
@@ -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,14 @@
namespace Francesco.Recipes.World.Models
{
using Francesco.Recipes.World.Models.BackendModels.Ingredient;
using Francesco.Recipes.World.Models.BackendModels.Unit;
public class IngredientViewModel
{
public Guid RecipeId { get; set; }
public List<Ingredient> Ingredients { get; set; } = new List<Ingredient>();
public List<Unit> Units { get; set; } = new List<Unit>();
}
}
@@ -0,0 +1,13 @@
using Francesco.Recipes.World.Models.BackendModels.Instruction;
namespace Francesco.Recipes.World.Models
{
public class InstructionViewModel
{
public Guid RecipeId { get; set; }
public string Description { get; set; } = string.Empty;
public List<Instruction> Instructions { get; set; } = new List<Instruction>();
}
}
@@ -0,0 +1,9 @@
namespace Francesco.Recipes.World.Models
{
public interface IFavoritable
{
Guid Id { get; set; }
bool IsFavorite { get; set; }
}
}
@@ -0,0 +1,17 @@
namespace Francesco.Recipes.World.Models
{
public class RecipeCardViewModel : IFavoritable
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public TimeSpan CookingTime { get; set; }
public bool IsFavorite { get; set; }
public byte[]? ImageData { get; set; }
public string? MimeType { get; set; }
}
}
@@ -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; }
}
}
@@ -0,0 +1,13 @@
using Francesco.Recipes.World.Models.BackendModels.Recipe;
namespace Francesco.Recipes.World.Models
{
public class ShoppingListDetailsViewModel
{
public int RecipeCount { get; set; }
public List<Recipe> RecipesInAnyShoppingList { get; set; } = new List<Recipe>();
public Dictionary<Guid, Guid> RecipeIngredientToShoppingListMap { get; set; } = new Dictionary<Guid, Guid>();
}
}
+72
View File
@@ -0,0 +1,72 @@
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Repositories.Category;
using Francesco.Recipes.World.Repositories.Favorit;
using Francesco.Recipes.World.Repositories.Ingredient;
using Francesco.Recipes.World.Repositories.Instruction;
using Francesco.Recipes.World.Repositories.MediaFile;
using Francesco.Recipes.World.Repositories.Recipe;
using Francesco.Recipes.World.Repositories.ShoppingList;
using Francesco.Recipes.World.Repositories.Unit;
using Francesco.Recipes.World.Services.Instruction;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var connectionString = builder.Configuration.GetConnectionString("FrancescosRecipesWorldDbContextConnection")
?? throw new InvalidOperationException("Connection string 'FrancescosRecipesWorldDbContextConnection' not found.");
services.AddDbContext<FrancescosRecipesWorldDbContext>(options =>
options.UseSqlServer(connectionString, sqlOptions =>
sqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)));
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddScoped<IRecipeRepository, RecipeRepository>();
builder.Services.AddScoped<IIngredientRepository, IngredientRepository>();
builder.Services.AddScoped<IUnitRepository, UnitRepository>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddScoped<IShoppingListRepository, ShoppingListRepository>();
builder.Services.AddScoped<IMediaFileRepository, MediaFileRepository>();
builder.Services.AddScoped<IInstructionRepository, InstructionRepository>();
builder.Services.AddScoped<IFavoriteRepository, FavoritRepository>();
builder.Services.AddScoped<IInstructionService, InstructionService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
Console.WriteLine("Standard Numeric Format Specifiers");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapDefaultControllerRoute();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Category}/{action=Index}/{id?}");
app.Run();
@@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:54294",
"sslPort": 44322
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5103",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7171;http://localhost:5103",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,89 @@
namespace Francesco.Recipes.World.Repositories.Category
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models;
using Francesco.Recipes.World.Models.BackendModels.Category;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Views.Category;
using Microsoft.EntityFrameworkCore;
public class CategoryRepository : ICategoryRepository
{
private readonly FrancescosRecipesWorldDbContext _context;
public CategoryRepository(FrancescosRecipesWorldDbContext context)
{
_context = context;
}
public async Task<Category> GetCategoryByIdAsync(Guid categoryId)
{
var category = await _context.Categories.FindAsync(categoryId);
return category ?? throw new InvalidDataException($"Category {categoryId} not found.");
}
public async Task<IEnumerable<Category>> GetAllCategoriesAsync()
{
return await _context.Categories.ToListAsync();
}
public async Task<IEnumerable<Recipe>> GetRecipesByCategoryAsync(Guid categoryId)
{
var category = await _context.Categories
.Include(c => c.Recipes)
.FirstOrDefaultAsync(c => c.Id == categoryId);
if (category == null)
{
throw new InvalidDataException($"Category {categoryId} not found.");
}
return category.Recipes;
}
public async Task<IEnumerable<Category>> GetAllCategoriesWithRecipesAsync()
{
var categories = await _context.Categories
.Include(c => c.Recipes.OrderByDescending(r => r.CreatedAt).Take(3))
.ThenInclude(r => r.MediaFiles.Take(2))
.AsSplitQuery()
.ToListAsync();
return categories;
}
public async Task<IEnumerable<CategoryRecipesViewModel>> GetAllCategoriesWithRecipesViewModelAsync()
{
var categories = await _context.Categories
.Select(c => new CategoryRecipesViewModel
{
Category = c,
Recipes = c.Recipes
.OrderByDescending(r => r.CreatedAt)
.Take(3)
.Select(r => new RecipeCardViewModel
{
Id = r.Id,
Name = r.Name,
CookingTime = r.CookingTime,
IsFavorite = r.IsFavorite,
ImageData = r.MediaFiles
.OrderBy(m => m.Id)
.Select(m => m.Data)
.FirstOrDefault(),
MimeType = r.MediaFiles
.OrderBy(m => m.Id)
.Select(m => m.MimeType)
.FirstOrDefault(),
}),
})
.AsSplitQuery()
.ToListAsync();
return categories;
}
}
}
@@ -0,0 +1,22 @@
namespace Francesco.Recipes.World.Repositories.Category
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Francesco.Recipes.World.Models.BackendModels.Category;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Francesco.Recipes.World.Views.Category;
public interface ICategoryRepository
{
Task<Category> GetCategoryByIdAsync(Guid categoryId);
Task<IEnumerable<Category>> GetAllCategoriesAsync();
Task<IEnumerable<Recipe>> GetRecipesByCategoryAsync(Guid categoryId);
Task<IEnumerable<Category>> GetAllCategoriesWithRecipesAsync();
Task<IEnumerable<CategoryRecipesViewModel>> GetAllCategoriesWithRecipesViewModelAsync();
}
}
@@ -0,0 +1,72 @@
namespace Francesco.Recipes.World.Repositories.Favorit
{
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
using Microsoft.EntityFrameworkCore;
public class FavoritRepository : IFavoriteRepository
{
private readonly FrancescosRecipesWorldDbContext _context;
public FavoritRepository(FrancescosRecipesWorldDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Recipe>> GetFavoriteRecipesAsync()
{
return await _context.Recipes
.Where(r => r.IsFavorite)
.Include(r => r.Favorite)
.Include(r => r.MediaFiles)
.Take(6)
.ToListAsync();
}
public async Task<bool> IsFavoriteAsync(Guid recipeId)
{
return await _context.Recipes
.AnyAsync(r => r.Id == recipeId && r.IsFavorite);
}
public async Task AddFavoriteAsync(Guid recipeId)
{
var recipe = await _context.Recipes
.Include(r => r.Favorite)
.FirstOrDefaultAsync(r => r.Id == recipeId);
if (recipe == null)
{
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)
{
var recipe = await _context.Recipes.FindAsync(recipeId);
if (recipe != null && recipe.IsFavorite)
{
recipe.IsFavorite = false;
await _context.SaveChangesAsync();
}
}
}
}
@@ -0,0 +1,15 @@
namespace Francesco.Recipes.World.Repositories.Favorit
{
using Francesco.Recipes.World.Models.BackendModels.Recipe;
public interface IFavoriteRepository
{
Task<IEnumerable<Recipe>> GetFavoriteRecipesAsync();
Task<bool> IsFavoriteAsync(Guid recipeId);
Task AddFavoriteAsync(Guid recipeId);
Task RemoveFavoriteAsync(Guid recipeId);
}
}
@@ -0,0 +1,16 @@
namespace Francesco.Recipes.World.Repositories.Ingredient
{
using Francesco.Recipes.World.Models.BackendModels.Ingredient;
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
public interface IIngredientRepository
{
Task UpdateIngredientAsync(Ingredient ingredient);
Task<List<RecipeIngredient>> GetIngredientsByRecipeIdAsync(Guid recipeId);
Task<List<Ingredient>> GetIngredientsByNameAsync(string name);
Task<Ingredient> GetIngredientByIdAsync(Guid ingredientId);
}
}
@@ -0,0 +1,88 @@
namespace Francesco.Recipes.World.Repositories.Ingredient
{
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models.BackendModels.Ingredient;
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
using Microsoft.EntityFrameworkCore;
public class IngredientRepository : IIngredientRepository
{
private readonly FrancescosRecipesWorldDbContext _context;
public IngredientRepository(
FrancescosRecipesWorldDbContext context)
{
_context = context;
}
public async Task UpdateRecipeIngredientAsync(RecipeIngredient recipeIngredient)
{
if (recipeIngredient == null)
{
throw new ArgumentNullException(nameof(recipeIngredient));
}
var existingRecipeIngredient = await _context.RecipeIngredients
.Include(ri => ri.Ingredient)
.Include(ri => ri.Unit)
.FirstOrDefaultAsync(ri => ri.Id == recipeIngredient.Id);
if (existingRecipeIngredient == null)
{
throw new InvalidOperationException($"RecipeIngredient with ID {recipeIngredient.Id} not found.");
}
existingRecipeIngredient.Quantity = recipeIngredient.Quantity;
existingRecipeIngredient.Ingredient = recipeIngredient.Ingredient;
existingRecipeIngredient.Unit = recipeIngredient.Unit;
await _context.SaveChangesAsync();
}
public async Task<List<Ingredient>> GetIngredientsByNameAsync(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return await _context.Ingredients.ToListAsync();
}
return await _context.Ingredients
.Where(i => i.Name.ToLower().Contains(name.ToLower()))
.ToListAsync();
}
public async Task<List<RecipeIngredient>> GetIngredientsByRecipeIdAsync(Guid recipeId)
{
return await _context.RecipeIngredients
.Include(ri => ri.Ingredient)
.Include(ri => ri.Unit)
.Where(ri => ri.Recipe.Id == recipeId)
.ToListAsync();
}
public async Task UpdateIngredientAsync(Ingredient ingredient)
{
if (ingredient == null)
{
throw new ArgumentNullException(nameof(ingredient));
}
var existingIngredient = await _context.Ingredients.FindAsync(ingredient.Id);
if (existingIngredient == null)
{
throw new InvalidOperationException($"Ingredient with ID {ingredient.Id} not found.");
}
existingIngredient.Name = ingredient.Name;
await _context.SaveChangesAsync();
}
public async Task<Ingredient> GetIngredientByIdAsync(Guid ingredientId)
{
var ingredient = await _context.Ingredients.FindAsync(ingredientId);
return ingredient ?? throw new InvalidDataException($"Address {ingredientId} not found.");
}
}
}
@@ -0,0 +1,19 @@
namespace Francesco.Recipes.World.Repositories.Instruction
{
using Francesco.Recipes.World.Models.BackendModels.Instruction;
public interface IInstructionRepository
{
Task<Instruction> GetInstructionAsync(Guid instructionId);
Task<Instruction> CreateInstructionAsync(Guid recipeId, string description, IFormFile? photo);
Task RemoveInstructionFromRecipeAsync(Guid recipeId, Guid instructionId);
Task SwapInstructionNumbersAsync(Instruction a, Instruction b);
Task<List<Instruction>> GetInstructionsOfRecipeAsync(Guid recipeId);
Task RenumberInstructionsAsync(Guid recipeId);
}
}
@@ -0,0 +1,160 @@
namespace Francesco.Recipes.World.Repositories.Instruction
{
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models.BackendModels.Instruction;
using Francesco.Recipes.World.Models.BackendModels.MediaFile;
using Francesco.Recipes.World.Repositories.Recipe;
using Microsoft.EntityFrameworkCore;
public class InstructionRepository : IInstructionRepository
{
private readonly FrancescosRecipesWorldDbContext _context;
private readonly IRecipeRepository _recipeRepository;
public InstructionRepository(FrancescosRecipesWorldDbContext context, IRecipeRepository recipeRepository)
{
_recipeRepository = recipeRepository;
_context = context;
}
public async Task<Instruction> GetInstructionAsync(Guid instructionId)
{
var instruction = await _context.Instructions.FindAsync(instructionId);
return instruction ?? throw new InvalidDataException($"Instruction {instructionId} not found.");
}
public async Task<Instruction> CreateInstructionAsync(Guid recipeId, string description, IFormFile? photo)
{
if (string.IsNullOrWhiteSpace(description))
{
throw new ArgumentException("Description cannot be empty", nameof(description));
}
var recipe = await _context.Recipes
.Include(r => r.Instructions)
.FirstOrDefaultAsync(r => r.Id == recipeId);
if (recipe == null)
{
throw new ArgumentException("Recipe not found.", nameof(recipeId));
}
var nextNumber = 1;
if (recipe.Instructions != null && recipe.Instructions.Any())
{
nextNumber = recipe.Instructions.Max(i => i.Number) + 1;
}
var newInstruction = new Instruction
{
Id = Guid.NewGuid(),
Description = description,
Number = nextNumber,
Recipe = recipe,
};
_context.Instructions.Add(newInstruction);
await _context.SaveChangesAsync();
if (photo != null && photo.Length > 0)
{
using var memoryStream = new MemoryStream();
await photo.CopyToAsync(memoryStream);
var instructionImage = new MediaFile
{
FileName = photo.FileName,
MimeType = photo.ContentType,
Data = memoryStream.ToArray(),
Instruction = newInstruction,
Recipe = null,
};
_context.Add(instructionImage);
await _context.SaveChangesAsync();
}
return newInstruction;
}
public async Task RemoveInstructionFromRecipeAsync(Guid recipeId, Guid instructionId)
{
var recipe = await _recipeRepository.GetRecipeAsync(recipeId);
if (recipe.Instructions == null || !recipe.Instructions.Any())
{
await _context.Entry(recipe)
.Collection(r => r.Instructions)
.LoadAsync();
}
var instructionToRemove = recipe.Instructions?.FirstOrDefault(i => i.Id == instructionId);
if (instructionToRemove != null)
{
await _context.Entry(instructionToRemove)
.Collection(i => i.MediaFiles)
.LoadAsync();
if (instructionToRemove.MediaFiles != null && instructionToRemove.MediaFiles.Any())
{
_context.MediaFiles.RemoveRange(instructionToRemove.MediaFiles);
}
recipe.Instructions?.Remove(instructionToRemove);
await _context.SaveChangesAsync();
await RenumberInstructionsAsync(recipeId);
}
}
public async Task<List<Instruction>> GetInstructionsOfRecipeAsync(Guid recipeId)
{
var instructions = await _context.Instructions
.Include(i => i.MediaFiles)
.Where(i => i.Recipe.Id == recipeId)
.OrderBy(i => i.Number)
.ToListAsync();
if (!instructions.Any())
{
throw new InvalidDataException($"No instructions found for Recipe ID {recipeId}.");
}
return instructions;
}
public async Task RenumberInstructionsAsync(Guid recipeId)
{
var instructions = await _context.Instructions
.Where(i => i.Recipe.Id == recipeId)
.OrderBy(i => i.Number)
.ToListAsync();
for (var i = 0; i < instructions.Count; i++)
{
instructions[i].Number = i + 1;
}
await _context.SaveChangesAsync();
}
public async Task SwapInstructionNumbersAsync(Instruction a, Instruction b)
{
if (a == null)
{
throw new ArgumentNullException(nameof(a), "Instruction 'a' cannot be null.");
}
if (b == null)
{
throw new ArgumentNullException(nameof(b), "Instruction 'b' cannot be null.");
}
var temp = a.Number;
a.Number = b.Number;
b.Number = temp;
await _context.SaveChangesAsync();
}
}
}
@@ -0,0 +1,11 @@
namespace Francesco.Recipes.World.Repositories.MediaFile
{
public interface IMediaFileRepository
{
Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediaFileIdToReplace, string fileName, string mimeType, byte[] newMediaData);
Task UploadInstructionImageAsync(Guid instructionId, IFormFile? photo);
Task UploadRecipeMediaAsync(Guid recipeId, IFormFile mediaFile);
}
}
@@ -0,0 +1,136 @@
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;
using Francesco.Recipes.World.Repositories.Instruction;
using Francesco.Recipes.World.Repositories.Recipe;
public class MediaFileRepository : IMediaFileRepository
{
private readonly IInstructionRepository _instructionRepository;
private readonly FrancescosRecipesWorldDbContext _context;
private readonly IRecipeRepository _recipeRepository;
public MediaFileRepository(IInstructionRepository instructionRepository, FrancescosRecipesWorldDbContext context, IRecipeRepository recipeRepository)
{
_instructionRepository = instructionRepository;
_context = context;
_recipeRepository = recipeRepository;
}
public async Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediaFileIdToReplace, string fileName, string mimeType, byte[] newMediaData)
{
var instruction = await _instructionRepository.GetInstructionAsync(instructionId);
var mediaToReplace = instruction.MediaFiles.FirstOrDefault(m => m.Id == mediaFileIdToReplace);
if (mediaToReplace == null)
{
throw new InvalidOperationException("The specified media file does not exist.");
}
_context.MediaFiles.Remove(mediaToReplace);
await _context.SaveChangesAsync();
var newMedia = new MediaFile
{
Id = Guid.NewGuid(),
FileName = fileName,
MimeType = mimeType,
Data = newMediaData,
Instruction = instruction,
};
_context.MediaFiles.Add(newMedia);
await _context.SaveChangesAsync();
}
public async Task UploadInstructionImageAsync(Guid instructionId, IFormFile? photo)
{
if (photo is null)
{
throw new ArgumentNullException(nameof(photo));
}
var instruction = await _instructionRepository.GetInstructionAsync(instructionId);
using (var memoryStream = new MemoryStream())
{
await photo.CopyToAsync(memoryStream);
var instructionImage = new MediaFile
{
FileName = photo.FileName,
MimeType = photo.ContentType,
Data = memoryStream.ToArray(),
Instruction = instruction,
Recipe = null,
};
_context.Add(instructionImage);
await _context.SaveChangesAsync();
}
}
public async Task UploadRecipeMediaAsync(Guid recipeId, IFormFile mediaFile)
{
if (mediaFile == null || mediaFile.Length == 0)
{
return;
}
try
{
var recipe = await _recipeRepository.GetRecipeAsync(recipeId);
var isImage = mediaFile.ContentType.StartsWith(ContentType.Image);
var isVideo = mediaFile.ContentType.StartsWith("video/");
if (!isImage && !isVideo)
{
throw new InvalidOperationException("Only image or video files are allowed.");
}
if (isImage)
{
await RemoveExistingMediaAsync(recipe, ContentType.Image);
}
else
{
await RemoveExistingMediaAsync(recipe, "video/");
}
using var memoryStream = new MemoryStream();
await mediaFile.CopyToAsync(memoryStream);
var newMedia = new MediaFile
{
Id = Guid.NewGuid(),
FileName = mediaFile.FileName,
MimeType = mediaFile.ContentType,
Data = memoryStream.ToArray(),
Recipe = recipe,
Instruction = null,
};
_context.MediaFiles.Add(newMedia);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
throw new InvalidOperationException("An error occurred while uploading the media file.", ex);
}
}
private async Task RemoveExistingMediaAsync(Recipe recipe, string mediaTypePrefix)
{
var existingMedia = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType?.StartsWith(mediaTypePrefix) == true);
if (existingMedia != null)
{
_context.MediaFiles.Remove(existingMedia);
await _context.SaveChangesAsync();
}
}
}
}
@@ -0,0 +1,25 @@
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;
public interface IRecipeRepository
{
Task<Recipe> GetRecipeAsync(Guid recipeId);
Task<Recipe?> GetRecipeByIdAsync(Guid id);
Task CreateRecipeIngredientAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId);
Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId);
Task<IReadOnlyCollection<Recipe>> GetRecipesByDifficultyAsync(Difficulty? difficulty);
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
Task<bool> DeleteRecipeAsync(Guid recipeId);
Task<IEnumerable<SearchViewModel>> SearchInRecipesAndIngredients(string searchTerm);
}
}
@@ -0,0 +1,273 @@
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;
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
using Francesco.Recipes.World.Repositories.Ingredient;
using Francesco.Recipes.World.Repositories.Unit;
using Microsoft.EntityFrameworkCore;
public class RecipeRepository : IRecipeRepository
{
private readonly FrancescosRecipesWorldDbContext _context;
private readonly IIngredientRepository _ingredientRepository;
private readonly IUnitRepository _unitRepository;
public RecipeRepository(
FrancescosRecipesWorldDbContext context, IIngredientRepository ingredientRepository, IUnitRepository unitRepository)
{
_context = context;
_ingredientRepository = ingredientRepository;
_unitRepository = unitRepository;
}
public async Task<Recipe> GetRecipeAsync(Guid recipeId)
{
var recipe = await _context.Recipes.FindAsync(recipeId);
return recipe ?? throw new InvalidDataException($"Address {recipeId} not found.");
}
public async Task<Recipe?> GetRecipeByIdAsync(Guid recipeId)
{
return await _context.Recipes
.Include(r => r.RecipeIngredients)
.ThenInclude(ri => ri.Ingredient)
.Include(r => r.RecipeIngredients)
.ThenInclude(ri => ri.Unit)
.Include(r => r.MediaFiles)
.Include(r => r.Instructions)
.ThenInclude(i => i.MediaFiles)
.FirstOrDefaultAsync(r => r.Id == recipeId);
}
public async Task CreateRecipeIngredientAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId)
{
var recipe = await GetRecipeAsync(recipeId);
var unit = await _unitRepository.GetUnitByIdAsync(unitId);
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity), "Die Menge muss größer als 0 sein.");
}
var ingredients = await _ingredientRepository.GetIngredientsByNameAsync(ingredientName);
var exactMatch = ingredients
.FirstOrDefault(i => i.Name.Equals(ingredientName, StringComparison.OrdinalIgnoreCase));
Ingredient ingredient;
if (exactMatch == null)
{
ingredient = new Ingredient
{
Id = Guid.NewGuid(),
Name = ingredientName,
};
_context.Ingredients.Add(ingredient);
await _context.SaveChangesAsync();
}
else
{
ingredient = exactMatch;
}
var existingEntry = await _context.RecipeIngredients
.FirstOrDefaultAsync(ri => ri.Recipe.Id == recipeId && ri.Ingredient.Id == ingredient.Id);
if (existingEntry != null)
{
throw new InvalidOperationException("Das Rezept enthält diese Zutat bereits.");
}
var recipeIngredient = new RecipeIngredient
{
Recipe = recipe,
Ingredient = ingredient,
Unit = unit,
Quantity = quantity,
};
_context.Add(recipeIngredient);
await _context.SaveChangesAsync();
}
public async Task<Recipe> CreateRecipeForCategoryAsync(
Category category,
string name,
string description,
Difficulty difficulty,
int servings,
TimeSpan preparationTime,
TimeSpan cookingTime)
{
if (category == null)
{
throw new ArgumentNullException(nameof(category), "Category cannot be null.");
}
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name), "Name cannot be empty.");
}
if (servings <= 0)
{
throw new ArgumentOutOfRangeException(nameof(servings), "Servings must be greater than 0.");
}
var recipe = new Recipe
{
Id = Guid.NewGuid(),
Name = name,
Description = description,
Difficulty = difficulty,
Servings = servings,
PreparationTime = preparationTime,
CookingTime = cookingTime,
CreatedAt = DateTime.UtcNow,
Category = category,
};
_context.Recipes.Add(recipe);
await _context.SaveChangesAsync();
return recipe;
}
public async Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId)
{
var recipeIngredient = await _context.RecipeIngredients
.FirstOrDefaultAsync(ri => ri.Recipe.Id == recipeId && ri.Ingredient.Id == ingredientId);
if (recipeIngredient == null)
{
throw new ArgumentException("Diese Zutat ist nicht mit dem Rezept verknüpft.");
}
_context.RecipeIngredients.Remove(recipeIngredient);
await _context.SaveChangesAsync();
}
public async Task<IEnumerable<SearchViewModel>> SearchInRecipesAndIngredients(string searchTerm)
{
try
{
if (string.IsNullOrWhiteSpace(searchTerm))
{
return await _context.Recipes
.OrderByDescending(r => r.CreatedAt)
.Take(20)
.Select(SearchViewModelSelector())
.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)
{
if (!difficulty.HasValue)
{
return await _context.Recipes
.Include(r => r.Category)
.Include(r => r.RecipeIngredients)
.ThenInclude(ri => ri.Ingredient)
.ToListAsync();
}
return await _context.Recipes
.Where(r => r.Difficulty == difficulty.Value)
.Include(r => r.Category)
.Include(r => r.RecipeIngredients)
.ThenInclude(ri => ri.Ingredient)
.ToListAsync();
}
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.Favorite != null && recipe.Favorite.Id != Guid.Empty)
{
_context.Remove(recipe.Favorite);
}
_context.Recipes.Remove(recipe);
await _context.SaveChangesAsync();
return true;
}
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}%")));
}
}
}
@@ -0,0 +1,19 @@
namespace Francesco.Recipes.World.Repositories.ShoppingList
{
using Francesco.Recipes.World.Models.BackendModels.Shoppinglist;
public interface IShoppingListRepository
{
Task<ShoppingList> AddIngredientsToShoppingListAsync(Guid recipeId, List<Guid> ingredientIds);
Task RemoveRecipeIfEmptyAsync(Guid shoppingListRecipeId);
Task<int> CountAllRecipeShoppinglistsAsync();
Task<IList<ShoppingList>> GetAllShoppingListsAsync();
Task RemoveIngredientsFromShoppingListAsync(List<Guid> recipeIngredientShoppngListIds);
Task RemoveRecipeFromShoppingListAsync(Guid recipeShoppingListId);
}
}
@@ -0,0 +1,235 @@
namespace Francesco.Recipes.World.Repositories.ShoppingList
{
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList;
using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList;
using Francesco.Recipes.World.Models.BackendModels.Shoppinglist;
using Microsoft.EntityFrameworkCore;
public class ShoppingListRepository : IShoppingListRepository
{
private readonly FrancescosRecipesWorldDbContext _context;
public ShoppingListRepository(FrancescosRecipesWorldDbContext context)
{
_context = context;
}
public async Task<ShoppingList> AddIngredientsToShoppingListAsync(Guid recipeId, List<Guid> ingredientIds)
{
if (ingredientIds == null || !ingredientIds.Any())
{
throw new ArgumentNullException(nameof(ingredientIds));
}
var recipe = await _context.Recipes
.Include(r => r.RecipeIngredients)
.FirstOrDefaultAsync(r => r.Id == recipeId);
if (recipe == null)
{
throw new Exception("Recipe not found.");
}
var shoppingList = await _context.ShoppingLists
.Include(sl => sl.RecipeShoppingList)
.ThenInclude(rsl => rsl.SelectedIngredients)
.Include(sl => sl.RecipeShoppingList)
.ThenInclude(rsl => rsl.Recipe)
.FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == recipeId));
var recipeIngredients = await _context.RecipeIngredients
.Where(ri => ingredientIds.Contains(ri.Id) && ri.Recipe.Id == recipeId)
.ToListAsync();
if (!recipeIngredients.Any())
{
throw new Exception("No valid ingredients found.");
}
if (shoppingList == null)
{
shoppingList = new ShoppingList
{
Id = Guid.NewGuid(),
CreatedAt = DateTime.UtcNow,
RecipeShoppingList = new List<RecipeShoppingList>(),
};
var newRecipeList = new RecipeShoppingList
{
Id = Guid.NewGuid(),
Recipe = recipe,
SelectedIngredients = recipeIngredients.Select(ri => new RecipeIngredientShoppingList
{
Id = Guid.NewGuid(),
RecipeIngredient = ri,
IsChecked = false,
}).ToList(),
};
shoppingList.RecipeShoppingList.Add(newRecipeList);
_context.ShoppingLists.Add(shoppingList);
}
else
{
var existingRecipeList = shoppingList.RecipeShoppingList
.FirstOrDefault(rsl => rsl.Recipe.Id == recipeId);
if (existingRecipeList == null)
{
existingRecipeList = new RecipeShoppingList
{
Id = Guid.NewGuid(),
Recipe = recipe,
SelectedIngredients = new List<RecipeIngredientShoppingList>(),
};
shoppingList.RecipeShoppingList.Add(existingRecipeList);
}
foreach (var ri in recipeIngredients)
{
if (!existingRecipeList.SelectedIngredients.Any(si => si.RecipeIngredient.Id == ri.Id))
{
existingRecipeList.SelectedIngredients.Add(new RecipeIngredientShoppingList
{
Id = Guid.NewGuid(),
RecipeIngredient = ri,
IsChecked = false,
});
}
}
shoppingList.ModifiedAt = DateTime.UtcNow;
}
await _context.SaveChangesAsync();
return shoppingList;
}
public async Task<IList<ShoppingList>> GetAllShoppingListsAsync()
{
return await GetShoppingListQuery()
.OrderByDescending(sl => sl.CreatedAt)
.ToListAsync();
}
public async Task RemoveRecipeIfEmptyAsync(Guid shoppingListRecipeId)
{
var recipeEntry = await _context.RecipeShoppingLists
.Include(r => r.SelectedIngredients)
.Include(r => r.ShoppingList)
.ThenInclude(sl => sl.RecipeShoppingList)
.FirstOrDefaultAsync(r => r.Id == shoppingListRecipeId);
if (recipeEntry != null && !recipeEntry.SelectedIngredients.Any())
{
var shoppingList = recipeEntry.ShoppingList;
_context.RecipeShoppingLists.Remove(recipeEntry);
await _context.SaveChangesAsync();
if (shoppingList != null && (shoppingList.RecipeShoppingList == null || !shoppingList.RecipeShoppingList.Any()))
{
_context.ShoppingLists.Remove(shoppingList);
await _context.SaveChangesAsync();
}
}
}
public async Task RemoveRecipeFromShoppingListAsync(Guid recipeShoppingListId)
{
var recipeEntry = await _context.RecipeShoppingLists
.Include(r => r.SelectedIngredients)
.Include(r => r.ShoppingList)
.ThenInclude(sl => sl.RecipeShoppingList)
.FirstOrDefaultAsync(r => r.Id == recipeShoppingListId);
if (recipeEntry != null)
{
var shoppingList = recipeEntry.ShoppingList;
_context.RecipeIngredientsShoppingLists.RemoveRange(recipeEntry.SelectedIngredients);
_context.RecipeShoppingLists.Remove(recipeEntry);
await _context.SaveChangesAsync();
if (shoppingList != null)
{
await _context.Entry(shoppingList).Collection(sl => sl.RecipeShoppingList).LoadAsync();
if (!shoppingList.RecipeShoppingList.Any())
{
_context.ShoppingLists.Remove(shoppingList);
await _context.SaveChangesAsync();
}
}
}
}
public async Task<int> CountAllRecipeShoppinglistsAsync()
{
return await _context.RecipeShoppingLists
.CountAsync();
}
public async Task RemoveIngredientsFromShoppingListAsync(List<Guid> recipeIngredientShoppingListIds)
{
if (recipeIngredientShoppingListIds == null || !recipeIngredientShoppingListIds.Any())
{
throw new ArgumentNullException(nameof(recipeIngredientShoppingListIds));
}
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var affectedRecipeShoppingListIds = await _context.RecipeIngredientsShoppingLists
.Where(risl => recipeIngredientShoppingListIds.Contains(risl.Id))
.Select(risl => risl.RecipeShoppingList.Id)
.Distinct()
.ToListAsync();
foreach (var id in recipeIngredientShoppingListIds)
{
var entry = await _context.RecipeIngredientsShoppingLists.FindAsync(id);
if (entry != null)
{
_context.RecipeIngredientsShoppingLists.Remove(entry);
}
}
await _context.SaveChangesAsync();
foreach (var recipeShoppingListId in affectedRecipeShoppingListIds)
{
await RemoveRecipeIfEmptyAsync(recipeShoppingListId);
}
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
private IQueryable<ShoppingList> GetShoppingListQuery()
{
return _context.ShoppingLists
.Include(sl => sl.RecipeShoppingList)
.ThenInclude(rsl => rsl.Recipe)
.ThenInclude(r => r.MediaFiles)
.Include(sl => sl.RecipeShoppingList)
.ThenInclude(rsl => rsl.SelectedIngredients)
.ThenInclude(si => si.RecipeIngredient)
.ThenInclude(ri => ri.Ingredient)
.Include(sl => sl.RecipeShoppingList)
.ThenInclude(rsl => rsl.SelectedIngredients)
.ThenInclude(si => si.RecipeIngredient)
.ThenInclude(ri => ri.Unit);
}
}
}
@@ -0,0 +1,13 @@
namespace Francesco.Recipes.World.Repositories.Unit
{
using Francesco.Recipes.World.Models.BackendModels.Unit;
public interface IUnitRepository
{
Task<Unit> GetUnitByIdAsync(Guid unitId);
Task<Unit> AddUnitAsync(string name, string symbol);
Task<List<Unit>> GetAllUnitsAsync();
}
}
@@ -0,0 +1,43 @@
namespace Francesco.Recipes.World.Repositories.Unit
{
using Francesco.Recipes.World.Data;
using Francesco.Recipes.World.Models.BackendModels.Unit;
using Microsoft.EntityFrameworkCore;
public class UnitRepository : IUnitRepository
{
private readonly FrancescosRecipesWorldDbContext _context;
public UnitRepository(
FrancescosRecipesWorldDbContext context)
{
_context = context;
}
public async Task<Unit> GetUnitByIdAsync(Guid unitId)
{
var unit = await _context.Units.FindAsync(unitId);
return unit ?? throw new InvalidDataException($"Address {unitId} not found.");
}
public async Task<Unit> AddUnitAsync(string name, string symbol)
{
var unit = new Unit
{
Id = Guid.NewGuid(),
Name = name,
Symbol = symbol,
};
_context.Units.Add(unit);
await _context.SaveChangesAsync();
return unit;
}
public async Task<List<Unit>> GetAllUnitsAsync()
{
return await _context.Units.ToListAsync();
}
}
}
@@ -0,0 +1,13 @@
namespace Francesco.Recipes.World.Services.Instruction
{
using Francesco.Recipes.World.Models.BackendModels.Instruction;
public interface IInstructionService
{
Task MoveInstructionUpAsync(Guid recipeId, Guid instructionId);
Task MoveInstructionDownAsync(Guid recipeId, Guid instructionId);
Task<List<Instruction>> GetSortedInstructionsAsync(Guid recipeId);
}
}
@@ -0,0 +1,58 @@
namespace Francesco.Recipes.World.Services.Instruction
{
using Francesco.Recipes.World.Models.BackendModels.Instruction;
using Francesco.Recipes.World.Repositories.Instruction;
public class InstructionService : IInstructionService
{
private readonly IInstructionRepository _instructionRepository;
public InstructionService(IInstructionRepository instructionRepository)
{
_instructionRepository = instructionRepository;
}
public Task MoveInstructionUpAsync(Guid recipeId, Guid instructionId)
=> MoveInstructionAsync(recipeId, instructionId, moveUp: true);
public Task MoveInstructionDownAsync(Guid recipeId, Guid instructionId)
=> MoveInstructionAsync(recipeId, instructionId, moveUp: false);
public async Task<List<Instruction>> GetSortedInstructionsAsync(Guid recipeId)
{
var instructions = await _instructionRepository.GetInstructionsOfRecipeAsync(recipeId);
return instructions.OrderBy(i => i.Number).ToList();
}
private async Task MoveInstructionAsync(Guid recipeId, Guid instructionId, bool moveUp)
{
var instructions = await _instructionRepository.GetInstructionsOfRecipeAsync(recipeId);
var instruction = instructions.FirstOrDefault(i => i.Id == instructionId);
if (instruction == null)
{
throw new InvalidDataException($"Instruction with ID {instructionId} not found.");
}
var minStep = 1;
var maxStep = instructions.Max(i => i.Number);
if ((moveUp && instruction.Number == minStep) || (!moveUp && instruction.Number >= maxStep))
{
return;
}
// Instructions are ordered by ascending numbers (1, 2, 3, ...).
// Moving up means swapping with the instruction that has one number less (Number - 1).
// Moving down means swapping with the instruction that has one number more (Number + 1).
var targetNumber = moveUp ? instruction.Number - 1 : instruction.Number + 1;
var neighbor = instructions.FirstOrDefault(i => i.Number == targetNumber);
if (neighbor != null)
{
await _instructionRepository.SwapInstructionNumbersAsync(instruction, neighbor);
}
}
}
}
@@ -0,0 +1,12 @@
namespace Francesco.Recipes.World.Views.Category
{
using Francesco.Recipes.World.Models;
using Francesco.Recipes.World.Models.BackendModels.Category;
public class CategoryRecipesViewModel
{
public Category Category { get; set; } = new ();
public IEnumerable<RecipeCardViewModel> Recipes { get; set; } = new List<RecipeCardViewModel>();
}
}
@@ -0,0 +1,23 @@
@model Francesco.Recipes.World.Models.BackendModels.Category.Category
@{
ViewData["Title"] = "Category Details";
}
<h2>Category Details</h2>
<div>
<h4>Category</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
Name
</dt>
<dd class="col-sm-10">
@Model.Name
</dd>
</dl>
</div>
<div>
<a asp-action="Index" class="btn btn-primary">Back to List</a>
</div>
@@ -0,0 +1,28 @@
@model IEnumerable<Francesco.Recipes.World.Models.BackendModels.Category.Category>
@{
ViewData["Title"] = "Categories";
}
<h2>Categories</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var category in Model)
{
<tr>
<td>@category.Name</td>
<td>
<a asp-action="Details" asp-route-id="@category.Id" class="btn btn-primary">Details</a>
<a asp-action="Recipes" asp-route-id="@category.Id" class="btn btn-secondary">Recipes</a>
</td>
</tr>
}
</tbody>
</table>
@@ -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>
}
@@ -0,0 +1,81 @@
@model IEnumerable<Francesco.Recipes.World.Views.Category.CategoryRecipesViewModel>
@Html.AntiForgeryToken()
<div class="welcome-banner text-center mb-4">
<img src="/images/banner2.jpg" alt="Willkommen" class="img-fluid" />
<h1 class="mt-3">Willkommen in der Rezept-App</h1>
</div>
<form hx-get="/Home/Search"
hx-target="#search-results"
hx-trigger="keyup changed delay:300ms"
onsubmit="return false;"
class="mb-4">
<input type="text" name="term" class="form-control" placeholder="Suchen nach Rezepten oder Zutaten..." autocomplete="off" />
</form>
<div id="search-results" class="mt-4"></div>
@foreach (var category in Model)
{
<div class="category-section mb-5" id="category-@category.Category.Id">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>@category.Category.Name</h2>
<a href="/Category/Details/@category.Category.Id"
class="btn btn-link"
hx-get="/Category/Details/@category.Category.Id"
hx-target="#category-@category.Category.Id"
hx-swap="outerHTML">Alle @category.Category.Name-Rezepte anzeigen</a>
</div>
<div class="row">
@foreach (var recipe in category.Recipes)
{
var imageData = recipe.ImageData;
var mimeType = recipe.MimeType;
<div class="col-md-3 mb-4">
<div class="card h-100">
<div class="recipe-image">
@if (imageData != null && mimeType != null)
{
<img src="data:@mimeType;base64,@Convert.ToBase64String(imageData)" alt="@recipe.Name" class="card-img-top" />
}
else
{
<img src="/images/placeholder.png" alt="@recipe.Name" class="card-img-top" />
}
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title">@recipe.Name</h5>
<p class="card-text text-muted mb-2">@recipe.CookingTime</p>
<div id="favorite-button-@recipe.Id">
@await Html.PartialAsync("_FavoriteButton", recipe)
</div>
<a href="/Details/@recipe.Id"
class="btn btn-primary mt-auto">Details</a>
</div>
</div>
</div>
}
<div class="col-md-3 mb-4">
<div class="card h-100 text-center">
<div class="card-body d-flex flex-column justify-content-center">
<a href="/Create/@category.Category.Id"
class="btn btn-outline-primary">Rezept hinzufügen</a>
</div>
</div>
</div>
</div>
</div>
}
@@ -0,0 +1,6 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>
@@ -0,0 +1,75 @@
@model IEnumerable<Francesco.Recipes.World.Models.SearchViewModel>
@{
if (!Model.Any())
{
<p class="text-muted">Keine Ergebnisse gefunden.</p>
}
else
{
<div class="row">
@foreach (var recipe in Model)
{
<div class="col-md-4 mb-3">
<div class="card h-100 shadow-sm">
<div class="recipe-image">
@if (recipe.ImageData != null && recipe.MimeType != null)
{
<img src="data:@recipe.MimeType;base64,@Convert.ToBase64String(recipe.ImageData)"
alt="@recipe.Name"
class="card-img-top" />
}
else
{
<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.TotalTime.Hours h @recipe.TotalTime.Minutes min
</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="/Details/@recipe.Id" class="btn btn-primary btn-sm">Details</a>
</div>
</div>
</div>
</div>
}
</div>
}
}
@@ -0,0 +1,61 @@
@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe
@{
ViewData["Title"] = "Add Instructions";
}
<h1>Add Instructions to @Model.Name</h1>
<div class="instructions-list">
<h3>Existing Instructions</h3>
<ul>
@foreach (var instruction in Model.Instructions.OrderBy(i => i.Number))
{
<li>@instruction.Number. @instruction.Description</li>
}
</ul>
</div>
<div class="add-instruction-form">
<h3>Add New Instruction</h3>
<form id="instruction-form" method="post" asp-action="AddInstruction" asp-controller="Recipe">
<input type="hidden" name="recipeId" value="@Model.Id" />
<div class="form-group">
<label for="description">Description</label>
<input type="text" class="form-control" id="description" name="description" required />
</div>
<button type="button" class="btn btn-primary" onclick="addInstructionToRecipe()">Add Instruction</button>
</form>
</div>
@section Scripts {
<script>
async function addInstructionToRecipe() {
event.preventDefault();
var form = document.getElementById('instruction-form');
var recipeId = form.querySelector('input[name="recipeId"]').value;
var description = form.querySelector('input[name="description"]').value;
var token = form.querySelector('input[name="__RequestVerificationToken"]').value;
var response = await fetch('/' + recipeId + '/AddInstruction', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'RequestVerificationToken': token
},
body: `recipeId=${encodeURIComponent(recipeId)}&description=${encodeURIComponent(description)}`
});
if (response.ok) {
location.reload();
} else {
alert('Failed to add instruction.');
}
}
</script>
}
@@ -0,0 +1,114 @@
@model Francesco.Recipes.World.Models.CreateRecipeViewModel
@using Francesco.Recipes.World.Models.BackendModels.Recipe
@using Francesco.Recipes.World.Models
@{
ViewData["Title"] = "Erstelle Rezept";
}
<h1 class="mb-4">Erstelle Rezept</h1>
<form method="post" enctype="multipart/form-data" asp-controller="Recipe" asp-action="Create" asp-route-categoryId="@Model.CategoryId">
<div class="mb-4">
<h2>Allgemein</h2>
<div class="form-group">
<label asp-for="Name">Name</label>
<input asp-for="Name" class="form-control" required />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description">Beschreibung</label>
<textarea asp-for="Description" class="form-control"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Difficulty">Schwierigkeit</label>
<select asp-for="Difficulty" class="form-control" required>
<option value="">Schwierigkeit wählen</option>
@foreach (var difficulty in Enum.GetValues(typeof(Difficulty)))
{
<option value="@difficulty">@difficulty</option>
}
</select>
<span asp-validation-for="Difficulty" class="text-danger"></span>
</div>
<div class="form-row">
<div class="col-sm-4">
<label asp-for="Servings">Portion (pro Person)</label>
<input asp-for="Servings" type="number" min="1" class="form-control" required />
<span asp-validation-for="Servings" class="text-danger"></span>
</div>
<div class="col-sm-4">
<label>Vorbereitungszeit</label>
<div class="d-flex">
<div class="input-group mr-2">
<input asp-for="PrepHours" type="number" class="form-control" min="0" value="0" />
<div class="input-group-append">
<span class="input-group-text">h</span>
</div>
</div>
<div class="input-group">
<input asp-for="PrepMinutes" type="number" class="form-control" min="0" max="59" value="0" />
<div class="input-group-append">
<span class="input-group-text">min</span>
</div>
</div>
</div>
</div>
<div class="col-sm-4">
<label>Kochzeit</label>
<div class="d-flex">
<div class="input-group mr-2">
<input asp-for="CookHours" type="number" class="form-control" min="0" value="0" />
<div class="input-group-append">
<span class="input-group-text">h</span>
</div>
</div>
<div class="input-group">
<input asp-for="CookMinutes" type="number" class="form-control" min="0" max="59" value="0" />
<div class="input-group-append">
<span class="input-group-text">min</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mb-4">
<h2>Zutaten</h2>
@await Html.PartialAsync("_IngredientsPartial", Model.IngredientViewModel ?? new IngredientViewModel
{
RecipeId = Model.CategoryId,
Ingredients = new List<Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient>(),
Units = ViewBag.Units ?? new List<Francesco.Recipes.World.Models.BackendModels.Unit.Unit>()
})
</div>
<div class="mb-4">
<h2>Anweisungen</h2>
@await Html.PartialAsync("_GetInstructions", Model.InstructionViewModel ?? new InstructionViewModel
{
RecipeId = Model.CategoryId,
Instructions = new List<Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction>()
})
</div>
<div class="mb-4">
<h2>Bild/Video</h2>
<div class="form-group">
<label asp-for="Photo">Foto</label>
<input asp-for="Photo" class="form-control-file" accept="image/*" />
</div>
<div class="form-group">
<label asp-for="Video">Video</label>
<input asp-for="Video" class="form-control-file" accept="video/*" />
</div>
</div>
<div class="text-center mt-4">
<button type="submit" class="btn btn-success btn-lg">Rezept speichern</button>
<a asp-action="Index" asp-controller="Home" class="btn btn-secondary btn-lg ml-2">Abbrechen</a>
</div>
</form>
@@ -0,0 +1,227 @@
@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe
@{
ViewData["Title"] = "Recipe Details";
}
<h1>@Model.Name</h1>
<div class="recipe-details" data-recipe-id="@Model.Id">
<div class="recipe-image">
@if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null)
{
var mediaFile = Model.MediaFiles.First();
if (mediaFile.Data != null)
{
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)" alt="@Model.Name" class="img-fluid" />
}
}
</div>
<div class="recipe-info">
<p><strong>Description:</strong> @Model.Description</p>
<p><strong>Difficulty:</strong> @Model.Difficulty</p>
<p><strong>Preparation Time:</strong> @Model.PreparationTime</p>
<p><strong>Cooking Time:</strong> @Model.CookingTime</p>
</div>
@await Html.PartialAsync("_AdjustableIngredientsPartial", Model)
@await Html.PartialAsync("_RecipeInstructionGridPartial", new Francesco.Recipes.World.Models.InstructionViewModel
{
RecipeId = Model.Id,
Instructions = Model.Instructions.ToList()
})
<form hx-delete="@($"/{Model.Id}/Delete")"
hx-confirm="Sind Sie sicher, dass Sie dieses Rezept löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
hx-redirect="/">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Rezept löschen
</button>
</form>
</div>
@section Scripts {
<script>
document.addEventListener('DOMContentLoaded', function() {
const recipeId = '@Model.Id';
window.recipeId = recipeId;
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');
fetch('/ShoppingList/CreateOrAddIngredients', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
},
body: JSON.stringify({ recipeId: idToUse, ingredientIds: selectedIngredientIds })
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Failed to update shopping list');
})
.then(result => {
localStorage.setItem('shoppingListId', result.shoppingListId);
const showInfo = (msg) => {
if (window.Swal) {
Swal.fire({ icon: 'info', title: 'Hinweis', text: msg, timer: 1800, showConfirmButton: false });
} else { alert(msg); }
};
if (result.skippedCount && result.skippedCount > 0) {
if (result.addedCount && result.addedCount > 0) {
showInfo(`Einkaufsliste aktualisiert. ${result.skippedCount} Zutaten waren bereits vorhanden und wurden übersprungen.`);
} else {
showInfo('Diese Zutaten sind bereits in der Einkaufsliste vorhanden.');
}
} else {
showInfo('Einkaufsliste aktualisiert.');
}
})
.catch(error => {
if (window.Swal) {
Swal.fire({ icon: 'error', title: 'Fehler', text: 'Fehler beim Aktualisieren der Einkaufsliste.' });
} else {
alert('Fehler beim Aktualisieren der Einkaufsliste.');
}
});
};
});
</script>
}
@@ -0,0 +1,72 @@
@using Francesco.Recipes.World.Models.BackendModels.Recipe
@model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel
<h1>Rezepte nach Schwierigkeitsgrad</h1>
<div class="mb-3">
<a asp-controller="Recipe" asp-action="Create" class="btn btn-primary">Neues Rezept erstellen</a>
</div>
<div class="row mb-4">
<div class="col-md-6">
<form asp-controller="Recipe" asp-action="FilterByDifficulty" method="get" id="filterForm">
<div class="form-group">
<label asp-for="SelectedDifficulty" class="form-label">Schwierigkeitsgrad</label>
<select asp-for="SelectedDifficulty" asp-items="Html.GetEnumSelectList<Difficulty>()" class="form-select" onchange="submitForm()">
<option value="">Alle Schwierigkeitsgrade</option>
</select>
</div>
</form>
</div>
</div>
<div class="row">
@if (Model?.Recipes != null && Model.Recipes.Any())
{
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Beschreibung</th>
<th>Schwierigkeitsgrad</th>
<th>Portionen</th>
<th>Zubereitungszeit</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
@foreach (var recipe in Model.Recipes)
{
<tr>
<td>@recipe.Name</td>
<td>@(recipe.Description?.Length > 100 ? recipe.Description.Substring(0, 100) + "..." : recipe.Description)</td>
<td>@recipe.?Difficulty</td>
<td>@recipe.Servings</td>
<td>@($"{recipe.PreparationTime.TotalMinutes} Min.")</td>
<td>
<a asp-controller="Recipe" asp-action="Details" asp-route-id="@recipe.Id" class="btn btn-sm btn-info">Details</a>
<a asp-controller="Recipe" asp-action="Edit" asp-route-id="@recipe.Id" class="btn btn-sm btn-primary">Bearbeiten</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="col-12">
<p>Keine Rezepte gefunden.</p>
</div>
}
</div>
@section Scripts {
<script>
function submitForm() {
document.getElementById('filterForm').submit();
}
</script>
}
@@ -0,0 +1,12 @@
namespace Francesco.Recipes.World.Views.Recipe
{
using System.Collections.Generic;
using Francesco.Recipes.World.Models.BackendModels.Recipe;
public class FilterByDifficultyViewModel
{
public Difficulty? SelectedDifficulty { get; set; }
public IReadOnlyCollection<Recipe> Recipes { get; set; } = new List<Recipe>();
}
}
@@ -0,0 +1,20 @@
@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe
@{
ViewData["Title"] = "Remove Ingredient";
}
<h1>Remove Ingredient</h1>
<h3>Are you sure you want to remove the ingredient '@ViewBag.IngredientName' from this recipe?</h3>
<form asp-action="RemoveIngredientConfirmed" asp-route-categoryId="@ViewBag.CategoryId" asp-route-recipeId="@ViewBag.RecipeId" asp-route-ingredientId="@ViewBag.IngredientId" method="post">
<input type="hidden" name="categoryId" value="@ViewBag.CategoryId" />
<input type="hidden" name="recipeId" value="@ViewBag.RecipeId" />
<input type="hidden" name="ingredientId" value="@ViewBag.IngredientId" />
<div class="form-group">
<input type="submit" value="Remove" class="btn btn-danger" />
<a asp-action="Details" asp-route-id="@ViewBag.RecipeId" class="btn btn-secondary">Cancel</a>
</div>
</form>
@@ -0,0 +1,25 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@@ -0,0 +1,43 @@
@model IEnumerable<Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient>
<div class="recipe-ingredients">
<h3>Zutaten</h3>
<form id="ingredient-form">
<ul id="ingredient-list">
@foreach (var ingredient in Model)
{
<li id="ingredient-@ingredient.Id">
<input type="checkbox" name="ingredientIds" value="@ingredient.Id" />
@ingredient.Ingredient.Name - @ingredient.Quantity @ingredient.Unit.Symbol
</li>
}
</ul>
<button type="button" class="btn btn-primary" onclick="addSelectedIngredientsToShoppingList()">Ausgewählte zur Einkaufsliste hinzufügen</button>
</form>
</div>
@section Scripts {
<script>
function addSelectedIngredientsToShoppingList() {
var form = document.getElementById('ingredient-form');
var formData = new FormData(form);
var selectedIngredientIds = formData.getAll('ingredientIds');
fetch('/ShoppingList/AddSelectedIngredients', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ingredientIds: selectedIngredientIds })
}).then(response => {
if (response.ok) {
alert('Ausgewählte Zutaten wurden zur Einkaufsliste hinzugefügt.');
} else {
alert('Fehler beim Hinzufügen der ausgewählten Zutaten zur Einkaufsliste.');
}
}).catch(error => {
alert('Ein Fehler ist aufgetreten: ' + error.message);
});
}
</script>
}
@@ -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,26 @@
@model Francesco.Recipes.World.Models.IFavoritable
<form hx-post="@Url.Action(Model.IsFavorite ? "RemoveFavorite" : "AddFavorite", "Recipe")"
hx-target="this"
hx-swap="outerHTML">
@Html.AntiForgeryToken()
<input type="hidden" name="recipeId" value="@Model.Id" />
<button type="submit" class="btn btn-link p-0" style="width: 40px; height: 40px;">
@if (Model.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>
@@ -0,0 +1,39 @@
@model Francesco.Recipes.World.Models.InstructionViewModel
@Html.AntiForgeryToken()
<div id="instructions-container">
@for (int i = 0; i < Model.Instructions.Count; i++)
{
<div class="instruction-item" id="instruction-@Model.Instructions[i].Id">
<div class="instruction-controls">
@if (Model.Instructions[i].MediaFiles != null && Model.Instructions[i].MediaFiles.Any())
{
var mediaFile = Model.Instructions[i].MediaFiles.First();
if (mediaFile.Data != null)
{
<div class="instruction-media">
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)"
alt="Instruction Media" class="instruction-media-preview" />
</div>
}
}
<input type="file" name="InstructionViewModel.Instructions[@i].MediaFile" class="instruction-file" />
<textarea name="InstructionViewModel.Instructions[@i].Description" placeholder="Beschreibung" class="form-control">@Model.Instructions[i].Description</textarea>
<button type="button" class="btn-delete" onclick="Francesco.removeInstruction('@Model.Instructions[i].Id', '@Model.RecipeId')">🗑️</button>
</div>
<div class="instruction-actions">
<button type="button" class="btn-move-up" onclick="Francesco.moveInstructionUp('@Model.Instructions[i].Id', '@Model.RecipeId')">⬆️</button>
<button type="button" class="btn-move-down" onclick="Francesco.moveInstructionDown('@Model.Instructions[i].Id', '@Model.RecipeId')">⬇️</button>
</div>
</div>
}
</div>
<button type="button" class="btn-add" onclick="Francesco.addInstruction()">Schritte hinzufügen</button>
<script src="~/js/site.js" asp-append-version="true" defer></script>
@@ -0,0 +1,29 @@
@model Francesco.Recipes.World.Models.IngredientViewModel
@Html.AntiForgeryToken()
<div id="ingredients-container">
@for (int i = 0; i < Model.Ingredients.Count; i++)
{
<div class="ingredient-item" id="ingredient-@Model.Ingredients[i].Id">
<div class="ingredient-controls">
<input type="text" name="IngredientViewModel.Ingredients[@i].Name" value="@Model.Ingredients[i].Name" placeholder="Name" class="form-control" />
<input type="number" name="IngredientViewModel.Ingredients[@i].RecipeIngredients[0].Quantity" value="@(Model.Ingredients[i].RecipeIngredients.FirstOrDefault()?.Quantity)" placeholder="Menge" class="form-control" />
<select name="IngredientViewModel.Ingredients[@i].RecipeIngredients[0].Unit.Id" class="form-control">
@foreach (var unit in Model.Units)
{
@if (Model.Ingredients[i].RecipeIngredients.FirstOrDefault()?.Unit?.Id == unit.Id)
{
<option value="@unit.Id" selected>@unit.Name (@unit.Symbol)</option>
}
else
{
<option value="@unit.Id">@unit.Name (@unit.Symbol)</option>
}
}
</select>
<button type="button" class="btn-delete" onclick="Francesco.removeIngredient('@Model.Ingredients[i].Id')">🗑️</button>
</div>
</div>
}
</div>
<button type="button" class="btn-add" onclick="Francesco.addIngredient()">Zutat hinzufügen</button>
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Francesco.Recipes.World</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/Francesco.Recipes.World.styles.css" asp-append-version="true"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Francesco.Recipes.World</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Startseite</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="ShoppingList" asp-action="Details">Einkaufsliste</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Favorite" asp-action="Index">Favoriten</a>
</li>
</ul>
<div class="d-flex">
<button class="btn btn-outline-dark me-2" id="darkModeToggle">
<i class="bi bi-moon"></i>
</button>
<button class="btn btn-outline-light" id="lightModeToggle">
<i class="bi bi-sun"></i>
</button>
</div>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" integrity="sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
let recipeId;
@if (ViewData["RecipeId"] != null)
{
<text>recipeId = '@ViewData["RecipeId"]';</text>
}
else if (ViewData["CategoryId"] != null)
{
<text>recipeId = '@ViewData["CategoryId"]';</text>
}
const darkModeToggle = document.getElementById('darkModeToggle');
const lightModeToggle = document.getElementById('lightModeToggle');
darkModeToggle.addEventListener('click', () => {
document.body.classList.add('bg-dark', 'text-white');
});
lightModeToggle.addEventListener('click', () => {
document.body.classList.remove('bg-dark', 'text-white');
});
document.body.addEventListener('htmx:configRequest', (event) => {
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
if (token) {
event.detail.headers['RequestVerificationToken'] = token;
}
});
</script>
@await Html.PartialAsync("_ValidationScriptsPartial")
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
@@ -0,0 +1,48 @@
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}
@@ -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>
@@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
@@ -0,0 +1,107 @@
@model Francesco.Recipes.World.Models.ShoppingListDetailsViewModel
@{
ViewData["Title"] = "Einkaufsliste Details";
}
@using System.Text.Json
@{
var recipeIngredientsJson = JsonSerializer.Serialize(
Model.RecipesInAnyShoppingList.ToDictionary(
r => r.Id,
r => r.RecipeIngredients.Select(ri => new
{
id = ri.Id,
name = ri.Ingredient.Name,
amount = ri.Quantity,
unit = ri.Unit.Symbol,
shoppingListId = Model.RecipeIngredientToShoppingListMap.ContainsKey(ri.Id)
? Model.RecipeIngredientToShoppingListMap[ri.Id]
: Guid.Empty
})
)
);
}
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Einkaufsliste Details</h2>
<div style="border:2px solid black; border-radius:30px; padding:10px 30px; display:inline-block;">
Anzahl Rezepte: <span id="recipeCount">@Model.RecipeCount</span>
</div>
</div>
<div class="d-flex align-items-center mb-4">
<button class="btn btn-outline-secondary me-2" type="button" id="carouselLeft">
<i class="bi bi-arrow-left"></i>
</button>
<div class="flex-grow-1 overflow-auto" style="white-space:nowrap;" id="recipeCarousel">
@foreach (var recipe in Model.RecipesInAnyShoppingList)
{
var mediaFile = recipe.MediaFiles?.FirstOrDefault();
var imageData = mediaFile?.Data;
var mimeType = mediaFile?.MimeType;
var ingredientCount = recipe.RecipeIngredients?.Count() ?? 0;
<div class="card d-inline-block mx-2 recipe-card"
data-recipe-id="@recipe.Id"
style="width: 180px; vertical-align:top; border: 1px solid #000;">
<div class="position-relative">
<div class="position-absolute top-0 end-0 p-2">
<button class="btn btn-sm btn-link text-dark" title="Rezept entfernen"
onclick="Francesco.removeRecipe('@recipe.Id');">
<i class="bi bi-trash" style="font-size: 1.2rem;"></i>
</button>
</div>
<div class="text-center p-2" style="height: 120px; display: flex; align-items: center; justify-content: center;">
@if (imageData != null && mimeType != null)
{
<img src="data:@mimeType;base64,@Convert.ToBase64String(imageData)"
alt="@recipe.Name"
style="max-height: 100%; max-width: 100%; object-fit: contain;" />
}
else
{
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-image" style="font-size: 3rem; color: #ccc;"></i>
</div>
}
</div>
<div class="p-2 text-center" style="border-top: 1px solid #000; background-color: #fff;">
<div class="recipe-name" style="font-weight: bold; font-size: 0.9rem; white-space: normal; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; height: 40px;">@recipe.Name</div>
</div>
</div>
</div>
}
</div>
<button class="btn btn-outline-secondary ms-2" type="button" id="carouselRight">
<i class="bi bi-arrow-right"></i>
</button>
</div>
<div id="ingredientListContainer" class="mt-4">
<div class="card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Zutaten</h4>
</div>
<div class="card-body">
<ul id="ingredientList" class="list-group"></ul>
<div class="text-center mt-3">
<button id="removeSelectedButton" class="btn" onclick="Francesco.removeSelectedIngredients()">
ENTFERNE MARKIERTE
</button>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@Html.AntiForgeryToken()
<script>
window.recipeIngredients = @Html.Raw(recipeIngredientsJson);
</script>
<script src="~/js/site.js"></script>
}
@@ -0,0 +1,3 @@
@using Francesco.Recipes.World
@using Francesco.Recipes.World.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

Some files were not shown because too many files have changed in this diff Show More