Repository: danroth27/BestForYouRecipes Branch: main Commit: 2fc4dfc06c44 Files: 48 Total size: 97.9 KB Directory structure: gitextract_78i9fwlz/ ├── .gitignore ├── BestForYouRecipes/ │ ├── BestForYouRecipes.csproj │ ├── Components/ │ │ ├── App.razor │ │ ├── Layout/ │ │ │ ├── MainLayout.razor │ │ │ ├── MainLayout.razor.css │ │ │ ├── NavSidebar.razor │ │ │ └── NavSidebar.razor.css │ │ ├── Pages/ │ │ │ ├── Home.razor │ │ │ └── RecipeDetails.razor │ │ ├── RecipeCard.razor │ │ └── Routes.razor │ ├── Data/ │ │ ├── InMemorySearchProvider.cs │ │ ├── RecipesStore.cs │ │ └── recipes.json │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── _Imports.razor │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot/ │ └── css/ │ ├── fabric-icons-inline.css │ ├── fonts/ │ │ └── LICENSE.txt │ └── site.css ├── BestForYouRecipes.Client/ │ ├── BestForYouRecipes.Client.csproj │ ├── Data/ │ │ ├── IRecipesStore.cs │ │ ├── Ingredient.cs │ │ ├── Recipe.cs │ │ └── RecipesStore.cs │ ├── IngredientsListEditor.razor │ ├── IngredientsListEditor.razor.css │ ├── IngredientsListEditor.razor.js │ ├── Pages/ │ │ ├── SubmitRecipe.razor │ │ ├── SubmitRecipe.razor.css │ │ └── SubmitRecipe.razor.js │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── RecipeEditor.razor │ └── _Imports.razor ├── BestForYouRecipes.sln ├── LICENSE ├── README.md └── StarRatings/ ├── Review.cs ├── ReviewExtensions.cs ├── StarRating.razor ├── StarRating.razor.css ├── StarRatingReviews.razor ├── StarRatingReviews.razor.css ├── StarRatings.csproj └── _Imports.razor ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ ================================================ FILE: BestForYouRecipes/BestForYouRecipes.csproj ================================================  net8.0 enable enable 8.0 ================================================ FILE: BestForYouRecipes/Components/App.razor ================================================ Best For You Organics ================================================ FILE: BestForYouRecipes/Components/Layout/MainLayout.razor ================================================ @inherits LayoutComponentBase
@Body
Best For You Organics, Co.
An unhandled error has occurred. Reload X
================================================ FILE: BestForYouRecipes/Components/Layout/MainLayout.razor.css ================================================ header { height: 130px; display: flex; flex-direction: column; align-items: center; } #logo { height: inherit; } footer { height: 72px; background: #435869; font-family: 'Playfair Display', serif; color: white; } #blazor-error-ui { background: lightyellow; bottom: 0; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed; width: 100%; z-index: 1000; } #blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; } ================================================ FILE: BestForYouRecipes/Components/Layout/NavSidebar.razor ================================================  ================================================ FILE: BestForYouRecipes/Components/Layout/NavSidebar.razor.css ================================================ nav { position: fixed; top: 0; left: 0; width: 4.5rem; padding: 1rem; box-sizing: border-box; border-bottom-right-radius: 1.2rem; color: white; background-color: #435869; box-shadow: 0 2px 4px 0px rgb(0 0 0 / 30%); z-index: 1; transition: width 0.1s ease-out; } nav:hover { width: 15rem; } ::deep a { display: flex; gap: 1rem; font-size: 1.5rem; padding: 0.5rem; color: #e1e1e1; text-decoration: none; align-items: center; white-space: nowrap; overflow: hidden; } ::deep a.active { color: #a6ffb4; } ::deep a:hover:not(.active) { color: white; } span { font-family: 'Playfair Display', serif; font-weight: normal; font-size: 1.4rem; } ================================================ FILE: BestForYouRecipes/Components/Pages/Home.razor ================================================ @page "/" @inject IRecipesStore RecipesStore @attribute [StreamRendering] @if (recipes is null) {

Loading recipes...

} else { } @code { [SupplyParameterFromQuery] public string? Query { get; set; } IEnumerable? recipes; protected override async Task OnInitializedAsync() { recipes = await RecipesStore.GetRecipes(Query); } } ================================================ FILE: BestForYouRecipes/Components/Pages/RecipeDetails.razor ================================================ @page "/recipe/{recipeId}" @inject IRecipesStore RecipesStore ←Back to all recipes
@if (recipe == null) {

Recipe not found!

} else { @recipe.Name

@recipe.Name

@recipe.SourceShort | Servings: @recipe.Servings

Ingredients

    @for (int i = 0; i < recipe.Ingredients.Length; i++) { string id = $"ingredient{i}";
  • }

Instructions

@foreach (var instruction in recipe.Instructions.Split("\n")) {

@instruction

}

Tags

@foreach (var tag in recipe.Tags) { @tag }
}
@code { Recipe? recipe; [Parameter, EditorRequired] public string RecipeId { get; set; } = default!; protected override async Task OnParametersSetAsync() { recipe = await RecipesStore.GetRecipe(RecipeId); } Task OnSubmitReview(Review review) { recipe!.Reviews.Insert(0, review); return RecipesStore.UpdateRecipe(recipe); } } ================================================ FILE: BestForYouRecipes/Components/RecipeCard.razor ================================================ 

@Recipe.Name

@Recipe.SourceShort

@code { [Parameter, EditorRequired] public Recipe Recipe { get; set; } = default!; } ================================================ FILE: BestForYouRecipes/Components/Routes.razor ================================================  ================================================ FILE: BestForYouRecipes/Data/InMemorySearchProvider.cs ================================================ using System.Globalization; namespace BestForYouRecipes.Data; public class InMemorySearchProvider { readonly Dictionary recipes; readonly Dictionary> searchIndex; public InMemorySearchProvider(Dictionary recipes) { this.recipes = recipes; // Build search index based on name, tags, and ingredients searchIndex = new Dictionary>(); foreach (var recipe in recipes.Values) { var terms = recipe.Name.ToLower(CultureInfo.CurrentCulture).Split() .Concat(recipe.Tags.Select(tag => tag.ToLower(CultureInfo.CurrentCulture))) .Concat(recipe.Ingredients.SelectMany(ingredient => ingredient.ToLower(CultureInfo.CurrentCulture).Split())) .GroupBy(term => term) .Select, (string Term, int TermCount)>(termGroup => (termGroup.Key, termGroup.Count())); foreach (var term in terms) { if (!searchIndex.ContainsKey(term.Term)) { searchIndex[term.Term] = new List<(string, int)>(); } searchIndex[term.Term].Add((recipe.Id, term.TermCount)); } } } public IEnumerable Search(string query) { return query.ToLower(CultureInfo.CurrentCulture).Split() .Where(term => !string.IsNullOrWhiteSpace(term)) .SelectMany(term => searchIndex.Keys .Where(key => key.StartsWith(term, StringComparison.CurrentCultureIgnoreCase)) .SelectMany(key => searchIndex[key])) .GroupBy(termCount => termCount.RecipeId, termCount => termCount.Count) .OrderByDescending(termCounts => termCounts.Sum()) .Select(termCounts => recipes[termCounts.Key]); } } ================================================ FILE: BestForYouRecipes/Data/RecipesStore.cs ================================================ using System.Collections.Concurrent; using System.Globalization; using System.Text.Json; namespace BestForYouRecipes.Data; public class RecipesStore : IRecipesStore { private readonly Dictionary recipes; private readonly ConcurrentDictionary images = new(); private InMemorySearchProvider searchProvider; public RecipesStore(IHostEnvironment hostEnvironment) { var jsonFileInfo = hostEnvironment.ContentRootFileProvider.GetFileInfo(Path.Combine("Data", "recipes.json")); using var jsonStream = jsonFileInfo.CreateReadStream(); var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, AllowTrailingCommas = true }; recipes = JsonSerializer.Deserialize>(jsonStream, jsonOptions)!; searchProvider = new InMemorySearchProvider(recipes); } public async Task> GetRecipes(string? query) { // Simulate DB query await Task.Delay(1000); return string.IsNullOrWhiteSpace(query) ? recipes.Values : searchProvider.Search(query); } public Task GetRecipe(string id) { recipes.TryGetValue(id, out var recipe); return Task.FromResult(recipe); } public Task UpdateRecipe(Recipe recipe) { recipes[recipe.Id] = recipe; return Task.FromResult(recipe); } public Task AddRecipe(Recipe recipe) { recipe.Id = (recipes.Count + 1).ToString(CultureInfo.InvariantCulture); recipes.Add(recipe.Id, recipe); searchProvider = new InMemorySearchProvider(recipes); return Task.FromResult(recipe.Id); } public async Task AddImage(Stream imageData) { using var ms = new MemoryStream(); await imageData.CopyToAsync(ms); var filename = Guid.NewGuid().ToString(); images[filename] = ms.ToArray(); return $"images/uploaded/{filename}"; } public Task DownloadImage(string filename, Stream stream) { return stream.WriteAsync(images[filename].AsMemory()).AsTask(); } } ================================================ FILE: BestForYouRecipes/Data/recipes.json ================================================ { "1": { "id": "1", "name": "Strawberries Romanov (La Madeleine copycat)", "source": "http:\/\/cookeatshare.com\/recipes\/la-madeleine-s-strawberries-romanov-318025", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 4, "comments": "", "reviews": [ { "rating": 2, "text": "Recipe instructions are not complete." }, { "rating": 1, "text": "Tastes like mud." }, { "rating": 5, "text": "Delicious!" } ], "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "Wash strawberries and cut the tops off. Let strawberries drain. Mix together heavy whipping cream, powdered sugar, and the brandy. Beat with a mixer till this becomes thick. Place strawberries into glasses and spoon over the sauce.\r\n", "ingredients": [ "2 tbsp powdered sugar\r", "1\/2 pt heavy whipping cream\r", "1 lb strawberries, (2 pints)\r", "4 tbsp brandy" ], "tags": [ "fruit", "dessert", "strawberries", "copycat", "untried" ] }, "2": { "id": "2", "name": "Tomato Basil Soup (La Madeleine copycat)", "source": "http:\/\/cookeatshare.com\/recipes\/la-madeleine-s-tomato-basil-soup-318026", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 4, "comments": "", "reviews": [ { "rating": 5, "text": "Super tasty soup and so easy to make! This is a perfect weeknight dinner." }, { "rating": 4, "text": "This was an easy meal and pretty tasty." }, { "rating": 5, "text": "One of my favorties!" } ], "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "Combine tomatoes, juice\/and or possibly stock in saucepan. Simmer 30 min. Puree, along with the basil leaves, in small batches, in blender or food processor or use an immersion blender in the pan.\r\n\r\nReturn to saucepan and add in cream and butter, while stirring over low heat. Garnish with basil leaves and serve with your favorite bread.", "ingredients": [ "4 c tomatoes, minced, peeled, and cored\r", "4 cup vegetable stock\r", "12 basil leaves\r", "1 c heavy cream\r", "1\/4 lb unsalted butter\r", "Salt\r", "1\/4 tsp black pepper" ], "tags": [ "soup", "tomatoes", "copycat", "main" ] }, "3": { "id": "3", "name": "Godiva Angel Pie", "source": "everyrecipe.com", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 12, "comments": "", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "TO MAKE SHELL: Preheat oven to 300 degrees. In mixer bowl, beat egg whites until foamy. Beat in salt and cream of tartar and beat until soft peaks form. Add sugar, a tablespoon at a time, beating after each addition. Continue until very stiff peaks form. Fold in nuts and vanilla. Lightly butter a metal 8-inch pie plate. (Do NOT use glass.) Spoon meringue into pan lightly, building up the edges to come at least 1\/2 inch above the side of the pan. Bake in preheated 300 degree oven for 35 to 40 minutes until light brown. Cool completely before adding filling. TO MAKE FILLING: Melt chocolate and water in a pan over hot, but not boiling, water, stirring occasionally, to blend. Add vanilla and chocolate mixture (cooled) to whipped cream and pile into meringue shell, swirling top with spatula. Be sure to cool the chocolate mixture before adding to the whipped cream. If it is too warm, the mixture will thin out. If this happens, chill until it is thick enough to put in shell.", "ingredients": [ "4 (1 oz.) squares Godiva cooking chocolate or 4 oz. Baker's German sweet chocolate\r", "1\/2 tsp vanilla\r", "1 cup pecans, finely chopped\r", "1\/2 cup sugar\r", "1\/8 tsp cream of tartar\r", "1\/8 tsp salt\r", "2 egg whites, at room temperature\r", "3 tbsp water\r", "1 tsp vanilla\r", "1 cup heavy cream, whipped" ], "tags": [ "chocolate", "dessert", "pie", "untried" ] }, "4": { "id": "4", "name": "Old Fashion Vegetable Soup", "source": "Lee Ann Foulger", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 6, "comments": "", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "In a large soup pot, brown ground beef and onion, drain off fat. Add remaining ingredients EXCEPT for last 3 items (canned vegetables). Simmer on low heat until vegetables are tender, about an hour. Add canned vegetables and simmer 5 minutes. Remove bay leaf and serve.", "ingredients": [ "1 pound ground beef\r", "1 cup onion, chopped\r", "1 cup potatoes, diced\r", "1 cup carrots, sliced\r", "1 cup celery, diced\r", "2 16oz can diced tomatoes in juice (or substitute 2 small cans of tomato sauce)\r", "4 cup water\r", "1\/4 tsp dried basil, crushed\r", "1\/4 tsp dried thyme\r", "1 bay leaf\r", "1\/2 tsp salt\r", "1\/4 tsp pepper\r", "1\/2 tsp garlic powder\r", "2 tablespoon beef bouillon\r", "1 16oz can corn\r", "1 16oz can green beans\r", "1 16oz can green peas" ], "tags": [ "soup", "beef", "family_recipes", "main" ] }, "5": { "id": "5", "name": "Flaky Buttermilk Biscuits", "source": "Cooks Illustrated (All-Time Best Recipes 2010)", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 12, "comments": "For soured milk, add 1 tbsp lemon juice or white vinegar to 1 cup milk", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "Adjust oven rack to lower-middle position; heat oven to 450 degrees. \r\n\r\nWhisk flour, baking power, baking soda, and salt in large bowl.\r\n\r\nAdd shortening to flour mixture; break up chunks with fingertips until only pea-sized pieces remain. Working in batches, drop butter slices into flour mixture and toss to coat; pick up each slice of butter and press between floured fingertips into flat, nickel-sized pieces. Repeat until all butter is incorporated; toss to combine. Freeze mixture (in bowl) until chilled, about 15 minutes.\r\n\r\nSpray 24-square-inch work surface with nonstick cooking spray; spread spray evenly across surface with kitchen towel or paper towel. Sprinkle 1\/3 cup of extra flour across sprayed area; gently spread flour across work surface with palm to form thin, even coating. Add all but 2 tablespoons of buttermilk to flour mixture; stir briskly with fork until ball forms and no dry bits of flour are visible, adding remaining buttermilk as needed (dough will be sticky and shaggy but should clear sides of bowl). With rubber spatula, transfer dough onto center of prepared work surface, dust surface lightly with flour, and, with floured hands, bring dough together into cohesive ball.\r\n\r\nPat dough into approximate 10-inch square; roll into 18- by 14-inch rectangle about 1\/4 inch thick, dusting dough and rolling pin with flour as needed. Using bench scraper or thin metal spatula, fold dough into thirds, brushing any excess flour from surface; lift short end of dough and fold in thirds again to form approximate 6- by 4-inch rectangle. Rotate dough 90 degrees, dusting work surface underneath with flour; roll and fold dough again, dusting with flour as needed.\r\n\r\nRoll dough into 10-inch square about 1\/2 inch thick; flip dough and cut nine 3-inch rounds with floured biscuit cutter, dipping cutter back into flour after each cut. Carefully invert and transfer rounds to ungreased baking sheet, spaced 1 inch apart. Gather dough scraps into ball; roll and fold once or twice until scraps form smooth dough. Roll dough into 1\/2-inch-thick round; cut 3 more 3-inch rounds and transfer to baking sheet. Discard excess dough.\r\n\r\nBrush biscuit tops with melted butter. Bake, without opening oven door, until tops are golden brown and crisp, 15 to 17 minutes. Let cool on baking sheet 5 to 10 minutes before serving.", "ingredients": [ "8 tablespoon unsalted butter, cold, lightly floured and cut into 1\/8-inch slices\r", "2 tablespoon vegetable shortening, cut into 1\/2-inch chunks\r", "1 teaspoon salt\r", "1 tablespoon baking powder\r", "1\/2 teaspoon baking soda\r", "2 1\/2 cup unbleached all-purpose flour\r", "2 tablespoon butter, melted\r", "1 1\/4 cup buttermilk, cold" ], "tags": [ "biscuits", "bread", "sides" ] }, "6": { "id": "6", "name": "The Ultimate Green Smoothie", "source": "Yuri Elkaim", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "", "calories": 427, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "To prepare, just throw everything in a blender and give it a whirl!", "ingredients": [ "1 apple\r", "1 banana\r", "3 leaves kale, stemmed\r", "1\/2 cup spinach\r", "1 scoop protein powder\r", "1 tbsp hemp seeds\r", "1 tsp maple syrup or agave\r", "1 lime, juiced\r", "2 cup water" ], "tags": [ "smoothie", "FUF", "breakfast" ] }, "7": { "id": "7", "name": "Tropical Nut Smoothie", "source": "Yuri Elkaim", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "", "calories": 628, "fat": 18, "satfat": 14, "carbs": 81, "fiber": 7, "sugar": 61, "protein": 42, "instructions": "Combine all ingredients in blender. Blend until smooth.", "ingredients": [ "1 cup pineapple chunks\r", "1 banana\r", "1\/4 cup coconut milk\r", "2 tbsp cocoa powder\r", "1 scoop protein powder (optional)\r", "2 cup skim milk" ], "tags": [ "smoothie", "FUF", "breakfast" ] }, "8": { "id": "8", "name": "Guacamole", "source": "Yuri Elkaim", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 8, "comments": "", "calories": 1552, "fat": 120, "satfat": 16, "carbs": 72, "fiber": 64, "sugar": 16, "protein": 24, "instructions": "1. Place all ingredients in a bowl with a flat bottom and mash it with a potato masher. Alternatively, you can mix everything briefly in a food processor.\r\n2. Stir well and keep refrigerated. This dish will brown on the areas exposed to air within a few hours, so serve it shortly after making it or just stir again before serving.", "ingredients": [ "4 ripe avocados\r", "2 roma tomates, diced fine\r", "1\/2 red onion, minced\r", "1\/2 handful cilantro, minced\r", "2 clove garlic, minced\r", "1 lime, juiced\r", "Pinch of sea salt\r", "1 jalapeno, seeded and minced fine (optional)" ], "tags": [ "dip", "appetizer", "mexican", "FUF", "sides" ] }, "9": { "id": "9", "name": "Sweet Almond Date Smoothie", "source": "Yuri Elkaim", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "", "calories": 716, "fat": 25, "satfat": 2, "carbs": 123, "fiber": 12, "sugar": 90, "protein": 9, "instructions": "Combine all ingredients in blender. Blend until smooth.", "ingredients": [ "1 banana\r", "2 tbsp almond butter\r", "6 dates\r", "1 tsp maple syrup\r", "2 tsp cinnamon\r", "2 cup almond milk, or skim milk" ], "tags": [ "smoothie", "FUF", "breakfast" ] }, "10": { "id": "10", "name": "Extreme Green Salad", "source": "figandcherry.com", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "(formally \"Spinach and avocado salad with sesame wasabi dressing\")", "calories": 558, "fat": 44, "satfat": 6, "carbs": 40, "fiber": 0, "sugar": 0, "protein": 13, "instructions": "1. Place all the salad ingredients, except the sesame seeds, into a large bowl or platter.\r\n\r\n2. Whisk together all the dressing ingredients in a small bowl. Drizzle over the salad and toss lightly to coat.\r\n\r\n3. Sprinkle with sesame seeds and serve.", "ingredients": [ "4 handful baby spinach leaves\r", "1 cup edamame\r", "1 large avocado, diced\r", "1\/2 cucumber, finely sliced into half moons\r", "3 tablespoon sesame seeds\r", "1\/2 lime (or orange if you prefer a sweeter dressing), juiced\r", "1\/2 teaspoon wasabi paste\r", "1 tablespoon soy sauce\r", "2 teaspoon sesame oil\r", "black pepper, to taste" ], "tags": [ "salad", "asian", "vegetarian", "main" ] }, "11": { "id": "11", "name": "Blueberry Bliss Smoothie", "source": "Yuri Elkaim", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "", "calories": 501, "fat": 3, "satfat": 1, "carbs": 82, "fiber": 6, "sugar": 62, "protein": 40, "instructions": "Combine all ingredients in blender. Blend until smooth.", "ingredients": [ "1 cup blueberries\r", "1 banana\r", "1 scoop protein powder\r", "1 tsp maple syrup\r", "2 cup skim milk" ], "tags": [ "FUF", "smoothie", "breakfast" ] }, "12": { "id": "12", "name": "Berry Sauce", "source": "Yuri Elkaim", "preptime": 0, "waittime": 0, "cooktime": 300, "servings": 1, "comments": "", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "1. Lightly warm berries and maple syrup in a saucepan\r\n2. Pour over pancakes\/dessert as desired", "ingredients": [ "1\/2 cup mixed berries\r", "2 tbsp maple syrup" ], "tags": [ "sauce", "components", "berries" ] }, "13": { "id": "13", "name": "Best Oatmeal Ever", "source": "Yuri Elkaim", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 1, "comments": "", "calories": 376, "fat": 14, "satfat": 2, "carbs": 57, "fiber": 9, "sugar": 21, "protein": 11, "instructions": "1. To prepare, once oatmeal has been sufficiently cooked, add diced apples and nut butter. Stir for 2-3 minutes.\r\n2. Serve into a bowl and mix in cinnamon, flax seeds, maple syrup\/agave. Stir and enjoy!", "ingredients": [ "1\/2 cup quick-cooking oats, cooked to your liking\r", "1\/2 apple, diced\r", "1\/2 tsp cinnamon\r", "1 tbsp maple syrup\r", "1 tbsp nut butter\r", "1 tbsp ground flax seed" ], "tags": [ "breakfast" ] }, "14": { "id": "14", "name": "Steakhouse steaks", "source": "http:\/\/www.foodnetwork.com\/recipes\/ina-garten\/steakhouse-steaks-recipe\/index.html", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "Preheat the oven to 400 degrees F.\r\n\r\nHeat a large, well-seasoned cast iron skillet over high heat until very hot, 5 to 7 minutes.\r\n\r\nMeanwhile, pat the steaks dry with a paper towel and brush them lightly with vegetable oil. Combine the fleur de sel and cracked pepper on a plate and roll the steaks in the mixture, pressing lightly to evenly coat all sides.\r\n\r\nWhen the skillet is ready, add the steaks and sear them evenly on all sides for about 2 minutes per side, for a total of 10 minutes.\r\n\r\nTop each steak with a tablespoon of butter, if using, and place the skillet in the oven. Cook the steaks until they reach 120 degrees F for rare or 125 degrees F for medium-rare on an instant-read thermometer. (To test the steaks, insert the thermometer sideways to be sure you're actually testing the middle of the steak.)\r\n\r\nRemove the steaks to a serving platter, cover tightly with aluminum foil and allow to rest at room temperature for 10 minutes. Serve hot with [Roquefort Cheese Sauce](http:\/\/www.xanthir.com\/recipes\/showrecipe.php?id=62) on the side.", "ingredients": [ "1 tablespoon coarsely cracked black peppercorns\r", "1 tablespoon fleur de sel\r", "2 tablespoon vegetable oil\r", "2 (10-ounce) filet mignon\r", "2 tablespoon unsalted butter, at room temperature, optional\r", "2 servings [Roquefort Cheese Sauce](http:\/\/www.xanthir.com\/recipes\/showrecipe.php?id=62)" ], "tags": [ "beef", "main" ] }, "15": { "id": "15", "name": "Handmade Pasta", "source": "napastyle.com", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 4, "comments": "", "calories": 961, "fat": 20, "satfat": 4, "carbs": 157, "fiber": 6, "sugar": 1, "protein": 35, "instructions": "Sift flour and semolina into a medium bow. Mix in salt. Add eggs and olive oil, and beginning in the center, mix with two fingers in a circular motion until flour is combined with eggs. Do not overmix. Remove dough from bowl and gather into a ball. On a lightly-floured board, knead gently with the heels of your hands, folding dough over onto itself until it forms a smooth mass. Pat into a ball, flatten slightly, wrap in waxed paper or plastic wrap, and refrigerate for 30 minutes or as long as overnight.\r\n\r\nWork on a lightly floured surface. Keep the dough lightly dusted with flour. Roll dough into a sheet about 1\/16-inch thick. Gently roll dough up into a cylinder. With a slicing knife, cut crosswise in 1\/4-inch slices. Dust dough with semolina and unwrap noodles. Cover with a tea towel until ready to cook. (The past may be made an hour ahead of time or frozen for up to 1 month. Defrost in refrigerator.) Fresh pasta will cook, in boiling salted water, in about 2-3 minutes.", "ingredients": [ "1 cup all-purpose flour\r", "1\/2 cup semolina\r", "1\/4 tsp salt\r", "2 eggs, at room temperature\r", "2 tsp extra-virgin olive oil" ], "tags": [ "noodles", "pasta", "components", "untried" ] }, "16": { "id": "16", "name": "Emeril Essence Creole Seasoning", "source": "Emeril Lagasse", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "Yield 2\/3 cup", "calories": 184, "fat": 4, "satfat": 1, "carbs": 39, "fiber": 15, "sugar": 9, "protein": 8, "instructions": "Combine all ingredients thoroughly.", "ingredients": [ "2 1\/2 tablespoon paprika\r", "2 tablespoon salt\r", "2 tablespoon garlic powder\r", "1 tablespoon black pepper\r", "1 tablespoon onion powder\r", "1 tablespoon cayenne pepper\r", "1 tablespoon dried oregano\r", "1 tablespoon dried thyme" ], "tags": [ "seasoning", "components", "untried" ] }, "17": { "id": "17", "name": "Natural sports drink", "source": "Yuri Elkhaim", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 1, "comments": "", "calories": 168, "fat": 0, "satfat": 0, "carbs": 44, "fiber": 0, "sugar": 36, "protein": 0, "instructions": "Mix together", "ingredients": [ "500 ml water\r", "1\/2 lemon, juiced\r", "1\/2 lime, juiced\r", "4 tbsp maple syrup\r", "pinch of sea salt" ], "tags": [ "FUF", "drink" ] }, "18": { "id": "18", "name": "Grilled garlic-lime fish tacos", "source": "Everyday Food magazine June 2011", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 4, "comments": "Yields 12 tacos", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "Heat a grill or grill pan to medium-high. Clean and lightly oil hot grill. In a medium bowl, whisk together olive oil, garlic, and lime juice. Add fish and turn once to coat; let sit 5 minutes. Remove fish from marinade, letting excess drip off. Season with salt and pepper and grill until flesh is opaque throughout, 12 minutes, flipping once. With a fork, break fish into large pieces and serve with tortillas and [salsa fresca](http:\/\/www.xanthir.com\/recipes\/showrecipe.php?id=id10).", "ingredients": [ "vegetable oil, for grill\r", "1 tablespoon extra-virgin olive oil\r", "2 clove garlic, finely chopped\r", "1 tablespoon fresh lime juice\r", "1 1\/4 pound skinless firm white-fleshed fish fillets, such as striped bass or cod (about 1 inch thick)\r", "coarse salt and ground pepper\r", "4 large flour tortillas, warmed or lightly toasted\r", "1 serving [Salsa fresca](http:\/\/www.xanthir.com\/recipes\/showrecipe.php?id=id172)" ], "tags": [ "tacos", "fish", "mexican", "grill", "main" ] }, "19": { "id": "19", "name": "Spicy Rice Casserole", "source": "Betty Crocker", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "", "calories": 1294, "fat": 46, "satfat": 12, "carbs": 186, "fiber": 14, "sugar": 26, "protein": 44, "instructions": "Preheat oven to 425 degrees.\r\n\r\nMix rice, oats, onion, bread crumbs, milk, basil, oregano, cayenne, and egg. Press mixture into small baking dish. Mix panko and parmesan, put on top of mixture. Bake for 20 minutes or until crispy or browned. (If topping starts browning too soon, cover loosely with aluminum foil.)\r\n\r\nServe with marinara sauce.", "ingredients": [ "2 cup cooked rice\r", "1\/2 cup quick-cooking oats\r", "1\/2 cup onion, chopped\r", "1\/4 cup dry bread crumbs\r", "1\/4 cup milk\r", "1 tbsp basil, chopped\r", "2 tsp oregano, chopped\r", "1\/4 tsp cayenne pepper\r", "1 large egg, beaten\r", "1\/2 cup panko\r", "1\/4 cup parmesan cheese\r", "4 serving [marinara sauce](http:\/\/www.xanthir.com\/recipes\/showrecipe.php?id=id34)" ], "tags": [ "vegetarian", "rice", "main" ] }, "20": { "id": "20", "name": "Straw and Hay Fettuccine Tangle", "source": "Super Natural Cooking by Heidi Swanson", "preptime": 900, "waittime": 0, "cooktime": 900, "servings": 4, "comments": "The asparagus puree can be made ahead of time; store in the refrigerator in a jar topped with a layer of olive oil.", "calories": 2338, "fat": 112, "satfat": 20, "carbs": 276, "fiber": 20, "sugar": 20, "protein": 88, "instructions": "Bring a large pot of water to a rolling boil.\r\n\r\nMeanwhile, make asparagus pesto.\r\n\r\nSalt the pasta water well and cook the pasta until just tender. Drain and toss immediately with 1 cup of the asparagus pesto (1\/4 cup per serving), stirring in more afterward depending on how heavily coated you like your pasta. Serve sprinkled with the remaining toasted pine nuts, a dusting of Parmesan, and a quick drizzle of extra-virgin olive oil.", "ingredients": [ "4 serving [asparagus pesto](http:\/\/www.xanthir.com\/recipes\/showrecipe.php?id=id436)", "1\/4 cup toasted pine nuts\r", "4 tbsp parmesan, grated\r", "12 oz dried pasta" ], "tags": [ "vegetarian", "pasta", "main" ] }, "21": { "id": "21", "name": "Green-packed Stir Fry with Fresh Herbs", "source": "Super Natural Cooking by Heidi Swanson", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 2, "comments": "", "calories": 770, "fat": 32, "satfat": 4, "carbs": 82, "fiber": 14, "sugar": 12, "protein": 40, "instructions": "Cook the rice, if it is not already.\r\n\r\nWhen you have all your ingredients prepped, arrange them within arm's reach of the stove. Heat a small splash of sesame oil in a wok or large nonstick pan over medium-high heat. When the oil is hot, add the tofu and cook for a couple of minutes, until the tofu is golden. Remove from the pan. (You can also cook the tofu in a dry nonstick or well seasoned pan.)\r\n\r\nAdd another splash of oil to the wok and, as soon as it's hot, add the asparagus and stir for 2 minutes. Add the white part of the green onions and chiles, and stir for another 2 minutes. Add the green part of the green onions, and stir for another 2 minutes. Add the garlic and ginger, and stir for 30 seconds, then add the cashews and spinach and stir for another minute, or until the spinach wilts and collapses. \r\n\r\nReturn the tofu to the pan. Stir in the stir-fry sauce. Cook for another minute, stirring constantly.\r\n\r\nRemove from heat and stir in the mint and basil. Season with enough soy sauce to make the flavors pop, starting with a splash.\r\n\r\nServe over cooked rice.\r\n\r\nNutrition information does not include stir-fry sauce.", "ingredients": [ "1 cup cooked rice\r", "Sesame oil\r", "8 oz extra-firm tofu, cut into slices 1-inch long and as thick as a pencil\r", "4 clove garlic, minced\r", "5 green onions, chopped with white and green parts separated\r", "1 tbsp fresh ginger, peeled and minced\r", "1\/2 jalapeno, deseeded and chopped\r", "1\/2 bunch thin asparagus (about 1\/2 pound), trimmed and cut diagonally into 1-inch slices\r", "1\/2 cup cashews, coarsely chopped\r", "2 1\/2 cup spinach leaves, stemmed\r", "2 tablespoon fresh mint, slivered\r", "2 tablespoon fresh basil, slivered\r", "2 servings stir-fry sauce" ], "tags": [ "vegetarian", "asian", "main" ] }, "22": { "id": "22", "name": "Hot Cocoa", "source": "Food Network Magazine", "preptime": 0, "waittime": 0, "cooktime": 300, "servings": 4, "comments": "", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "Prepare mix ahead of time. One serving is 1\/4 cup mix per cup of milk.\r\n\r\nBring milk to a simmer in a saucepan over medium heat. Whisk cocoa\/sugar mix into the milk. Add the chopped chocolate and reduce heat to low and simmer, stirring, until the chocolate melts and the cocoa is thick, about 2 minutes. Divide among mugs and garnish each with homemade marshmallows or whipped cream.", "ingredients": [ "3\/4 cup sugar\r", "1\/2 cup unsweetened cocoa powder\r", "1 tsp vanilla extract\r", "1\/4 tsp salt\r", "
", "4 cup milk\r", "4 oz chocolate, chopped fine" ], "tags": [ "drink", "chocolate" ] }, "23": { "id": "23", "name": "Oregano Marinated Chicken", "source": "http:\/\/www.foodnetwork.com\/recipes\/dave-lieberman\/greek-salad-with-oregano-marinated-chicken-recipe2\/index.html", "preptime": 1800, "waittime": 14400, "cooktime": 900, "servings": 6, "comments": "", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "To marinate the chicken: In a non-reactive dish, combine the lemon juice, olive oil, oregano, salt, and pepper and mix together. Add the chicken breasts to the dish and rub both sides in the mixture. Cover the dish with plastic wrap and let marinate in the refrigerator for at least 30 minutes and up to 4 hours.\r\n\r\nTo cook the chicken: Heat a nonstick skillet or grill pan over high heat. Add the chicken breasts and cook, turning once, until well browned, about 4 to 5 minutes on each side or until cooked through. Let the chicken rest on a cutting board for a few minutes before slicing it into thin strips.", "ingredients": [ "4 (6 to 7-ounce) boneless skinless chicken breasts\r", "10 grinds black pepper\r", "1\/2 tsp salt\r", "2 tablespoon extra-virgin olive oil\r", "1 teaspoon dried oregano\r", "1 lemon, juiced" ], "tags": [ "main", "chicken" ] }, "24": { "id": "24", "name": "Triple Grilled Cheese", "source": "Based on Food Network Magazine\u2019s \u201cTriple grilled cheese with tomato soup\u201d, Sept 2010.", "preptime": 600, "waittime": 0, "cooktime": 480, "servings": 4, "comments": "", "calories": 1848, "fat": 104, "satfat": 60, "carbs": 132, "fiber": 8, "sugar": 8, "protein": 100, "instructions": "Combine all three cheeses in a bowl. Divide evenly among 4 bread slices and top with the remaining bread. Heat 1 tablespoon butter in a large skillet or griddle over medium heat. Cook the sandwiches in batches, adding the remaining butter as needed, until the cheese melts and the bread is golden, 3-4 minutes per side. Serve the sandwiches with the soup.", "ingredients": [ "1 cup shredded muenster cheese (about 4 oz)\r", "1 cup shredded mozzarella cheese (about 4 oz)\r", "1\/2 cup grated Parmesan cheese (about 1 oz)\r", "8 slices thick sandwich bread\r", "2 tablespoon unsalted butter" ], "tags": [ "sandwich", "cheese", "main" ] }, "25": { "id": "25", "name": "Vegetarian Steamed Dumplings", "source": "http:\/\/www.foodnetwork.com\/recipes\/alton-brown\/vegetarian-steamed-dumplings-recipe\/index.html", "preptime": 0, "waittime": 0, "cooktime": 0, "servings": 4, "comments": "", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "Preheat the oven to 200 degrees F.\r\n\r\nCut the tofu in half horizontally and lay between layers of paper towels. Place on a plate, top with another plate, and place a weight on top (a 14-ounce can of vegetables works well). Let stand 20 minutes. After 20 minutes, cut the tofu into 1\/4-inch cubes and place in a large mixing bowl. Add the carrots, cabbage, red pepper, scallions, ginger, cilantro, soy sauce, hoisin, sesame oil, egg, salt, and pepper. Lightly stir to combine.\r\n\r\nTo form the dumplings, remove 1 wonton wrapper from the package, covering the others with a damp cloth. Brush the edges of the wrapper lightly with water. Place 1\/2 rounded teaspoon of the tofu mixture in the center of the wrapper. Shape as desired. Set on a sheet pan and cover with a damp cloth. Repeat procedure until all of the filling is gone.\r\n\r\nUsing a steaming apparatus of your choice, bring 1\/4 to 1\/2-inch of water to a simmer over medium heat. Spray the steamer's surface lightly with the non-stick vegetable spray to prevent sticking. Place as many dumplings as will fit into a steamer, without touching each other. Cover and steam for 10 to 12 minutes over medium heat. Remove the dumplings from the steamer to a heatproof platter and place in oven to keep warm. Repeat until all dumplings are cooked.\r\n\r\nTo make dipping sauce, whisk together ingredients in a small bowl.", "ingredients": [ "1\/2 pound firm tofu\r", "1\/2 cup carrots, coarsely grated\r", "1\/2 cup Napa cabbage, shredded\r", "2 tablespoon red bell pepper, finely chopped\r", "2 tablespoon green onions, finely chopped\r", "2 teaspoon fresh ginger, finely minced\r", "1 tablespoon cilantro, chopped\r", "1 tablespoon soy sauce\r", "1 tablespoon hoisin sauce\r", "2 teaspoon sesame oil\r", "1 egg, lightly beaten\r", "1 teaspoon kosher salt\r", "1\/4 teaspoon freshly ground black pepper\r", "Bowl of water, plus additional water for steamer\r", "40 small wonton wrappers\r", "Non-stick vegetable spray, for the steamer\r", "
\r", "1 1\/2 tablespoon soy sauce\r", "1 tablespoon seasoned rice vinegar\r", "1 teaspoon sesame oil\r", "1\/2 teaspoon sriracha" ], "tags": [ "vegetarian", "asian", "main" ] }, "26": { "id": "26", "name": "Wild Mushroom Risotto", "source": "Bride and Groom First and Forever Cookbook by Barber and Whiteford", "preptime": 900, "waittime": 0, "cooktime": 2700, "servings": 2, "comments": "", "calories": 1508, "fat": 70, "satfat": 20, "carbs": 166, "fiber": 8, "sugar": 20, "protein": 48, "instructions": "Bring the stock to a simmer in a small saucepan over medium-high heat. Reduce the heat to low, cover, and keep the stock hot.\r\n\r\nHeat the olive oil in a large heavy-bottomed saucepan over medium-high heat. Add the onion and saute, stirring frequently with a wooden spoon for 2 minutes. Add the mushrooms and cook until tender and slightly brown, about 8 minutes. Add the garlic and cook for 2 minutes more. Add the rice and stir well, about 30 seconds. Add the wine and simmer, stirring constantly until the liquid is absorbed, 2 to 3 minutes. Add 3\/4 cup of the hot stock, reduce the heat to medium, and cook, stirring frequently, until the stock is absorbed. Add the remaining hot stock 3\/4 cup at a time, stirring frequently and allowing each addition to be fully absorbed before adding more, until the rice is just tender and the risotto is creamy. The total cooking time is 20 to 25 minutes.\r\n\r\nStir in 1\/4 cup cheese, the thyme, and butter. If the rice appears dry, add up to 1\/4 cup water to loosen the risotto slightly. Season with salt and pepper to taste.\r\n\r\nDivide the risotto between 2 large bowls and drizzle with truffle oil, if desired. Sprinkle with the remaining 2 tablespoons cheese and the chives.", "ingredients": [ "3 1\/2 cup vegetable stock (or chicken stock)\r", "2 1\/2 tablespoon olive oil\r", "1 small yellow onion\r", "8 oz shiitake or cremini mushrooms, cleaned and sliced (if using shiitakes, discard stems)\r", "1 tbsp chopped garlic\r", "3\/4 cup arborio rice\r", "1\/2 cup dry white wine\r", "3\/8 cup Parmesan cheese, grated, divided use\r", "1\/2 tsp thyme\r", "1 tbsp unsalted butter\r", "Salt and freshly ground pepper\r", "Truffle oil, for drizzling, optional\r", "1 tbsp parsley, chopped" ], "tags": [ "main", "risotto", "mushrooms", "vegetarian" ] }, "27": { "id": "27", "name": "Emeril's strawberry lemonade", "source": "Everyday Food June 2011", "preptime": 600, "waittime": 2700, "cooktime": 120, "servings": 6, "comments": "", "calories": 0, "fat": 0, "satfat": 0, "carbs": 0, "fiber": 0, "sugar": 0, "protein": 0, "instructions": "1. In a medium saucepan, bring water and sugar to a boil over medium-high. Reduce to a simmer and cook, stirring occasionally, until sugar dissolves, 2 minutes. Transfer to a pitcher and refrigerated until cool, about 45 minutes. Add lemon juice and stir to combine.\r\n\r\n2. In a blender, puree strawberries until smooth. Pour through a fine-mesh sieve into a pitcher with lemon syrup, pressing on solid. Stir well to combine.\r\n\r\n3. To serve, stir in seltzer and divide among ice-filled glasses. Trop with lemon slices and mint.", "ingredients": [ "2 cup water\r", "1 cup sugar\r", "1 cup fresh lemon juice, (about 6 lemons=1 cup juice)\r", "1 lemon, sliced for serving\r", "1 pound strawberries, hulled and halved (about cups)\r", "16 oz seltzer, chilled\r", "mint, for serving" ], "tags": [ "drink", "untried" ], } } ================================================ FILE: BestForYouRecipes/Program.cs ================================================ using BestForYouRecipes; using BestForYouRecipes.Components; using BestForYouRecipes.Data; using BestForYouRecipes.Pages; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); builder.Services.AddSingleton(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/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(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAntiforgery(); app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(SubmitRecipe).Assembly); app.Map("images/uploaded/{filename}", (string filename, IRecipesStore recipeStore) => Results.Stream(body => recipeStore.DownloadImage(filename, body), "image/jpeg")); app.MapPost("api/recipes", async (Recipe recipe, IRecipesStore recipeStore) => await recipeStore.AddRecipe(recipe)); // TODO: Validate https://github.com/dotnet/aspnetcore/issues/46349 app.MapPost("api/images", async (Stream body, IRecipesStore recipeStore) => await recipeStore.AddImage(body)); app.Run(); ================================================ FILE: BestForYouRecipes/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:47750", "sslPort": 44341 } }, "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "http://localhost:5107", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:7241;http://localhost:5107", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: BestForYouRecipes/_Imports.razor ================================================ @using System.Linq.Expressions @using System.Net.Http @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using BestForYouRecipes @using BestForYouRecipes.Components @using BestForYouRecipes.Components.Layout @using BestForYouRecipes.Data @using StarRatings; ================================================ FILE: BestForYouRecipes/appsettings.Development.json ================================================ { "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } ================================================ FILE: BestForYouRecipes/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" } ================================================ FILE: BestForYouRecipes/wwwroot/css/fabric-icons-inline.css ================================================ /* Your use of the content in the files referenced here is subject to the terms of the license at https://aka.ms/fabric-assets-license */ @font-face { font-family: 'FabricMDL2Icons'; src: url('data:application/octet-stream;base64,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') format('truetype'); } .ms-Icon { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; display: inline-block; font-family: 'FabricMDL2Icons'; font-style: normal; font-weight: normal; speak: none; } .ms-Icon--Delete:before { content: "\E74D"; } .ms-Icon--EditListPencil:before { content: "\E61B"; } .ms-Icon--Home:before { content: "\E80F"; } .ms-Icon--Search:before { content: "\E721"; } .ms-Icon--Sort:before { content: "\E8CB"; } ================================================ FILE: BestForYouRecipes/wwwroot/css/fonts/LICENSE.txt ================================================ https://fonts.google.com/attribution ================================================ FILE: BestForYouRecipes/wwwroot/css/site.css ================================================ body { font-family: 'Open Sans', sans-serif; font-size: 16px; margin: 0; overflow-y: scroll; min-height: 100vh; display: flex; flex-direction: column; } body > * { padding: 20px 10%; } h1, h2, h3, h4, h5 { font-family: 'Playfair Display', serif; font-weight: normal; } h1 { color: #435896; text-align: center; } p, input { font-family: 'Open Sans', sans-serif; font-size: 16px; } input[type="checkbox"] { vertical-align: middle; } header { height: 130px; display: flex; flex-direction: column; align-items: center; } #logo { height: inherit; } .search { display: flex; align-items: center; max-width: 30em; margin: 0 auto; background: #F5F5F5; opacity: 0.78; color: #666666; border: 1px solid #999999; border-radius: 2px; padding: 6px 12px; } .search input { flex: 1; font-family: Open Sans; font-size: 14px; line-height: 20px; border: none; background: inherit; } .search input:focus { outline: none; } .search button { padding-left: 0.75rem; padding-right: 0.75rem; margin-right: -3px; cursor: pointer; } .recipe-list { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(18em, 1fr)); grid-gap: 20px; } .recipe-list-item { border-radius: 8px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.15); } .recipe-list-item a { text-decoration: none; color: inherit; } .recipe-list-item:hover { box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); } .recipe-card { box-sizing: border-box; height: 100%; display: flex; flex-direction: column; padding-bottom: 12px; } .recipe-card img { width: 100%; aspect-ratio: 508 / 472; object-fit: cover; } .recipe-card-body { flex: 1; display: flex; flex-direction: column; padding: 0px 8px 0px 8px; } .recipe-card-name { margin: 1rem 0 1rem 0; } .recipe-card-source { margin-top: 0; } .recipe-card .star-rating { margin-top: auto; justify-content: flex-end; } .back-link { font-family: 'Playfair Display', serif; text-transform: uppercase; color: #435869; text-decoration: none; } .source-and-servings { text-align: center; text-transform: uppercase; color: #212A31; text-align: center; margin: 5px 0; } .recipe > .star-rating-avg { text-align: center; margin: 5px auto; } .recipe-banner { display: block; width: 125%; margin: 0 -12.5%; aspect-ratio: 1261 / 508; object-fit: cover; } .recipe-details { padding: 0 5%; } .recipe-details ul { list-style: none; padding: 0; } .tag { color: inherit; font-size: 0.8em; text-decoration: none; background-color: #eaeaea; text-transform: uppercase; padding: 0 4px; } button { padding: 6px 25px; font-family: Playfair Display; color: white; background-color: #435869; border-radius: 2px; border: 0; } button:hover { background-color: #334452; } button:active { background-color: #26343f; } .review-editor { margin-bottom: 10px; } input.invalid, textarea.invalid, div.invalid { border: 1px solid red; } .validation-errors, .validation-message { color: red; margin-bottom: 0.5rem; } .reviews .rating { display: none; } footer { margin-top: auto; height: 72px; background: #435869; font-family: 'Playfair Display', serif; color: white; } #blazor-error-ui { background: lightyellow; bottom: 0; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed; width: 100%; z-index: 1000; } #blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 400; src: url('fonts/open-sans-v34-latin-regular.woff2') format('woff2'); } @font-face { font-family: 'Playfair Display'; font-style: normal; font-weight: 400; src: local(''), url('fonts/playfair-display-v30-latin-regular.woff2') format('woff2'); } .sortable-ghost { background-color: #ddd; } ================================================ FILE: BestForYouRecipes.Client/BestForYouRecipes.Client.csproj ================================================ net8.0 enable enable 8.0 ================================================ FILE: BestForYouRecipes.Client/Data/IRecipesStore.cs ================================================ namespace BestForYouRecipes; public interface IRecipesStore { Task> GetRecipes(string? query); Task GetRecipe(string id); Task UpdateRecipe(Recipe recipe); Task DownloadImage(string filename, Stream stream); Task AddRecipe(Recipe recipe); Task AddImage(Stream imageData); } ================================================ FILE: BestForYouRecipes.Client/Data/Ingredient.cs ================================================ using System.Text.RegularExpressions; namespace BestForYouRecipes; public class Ingredient { public double? Quantity { get; set; } // Optional because ingredients may be listed without a quantity (e.g., "seasoning") public IngredientUnit? Unit { get; set; } public required string Name { get; set; } public override string ToString() { if (Quantity.HasValue) { return Unit.HasValue ? $"{Quantity} {Unit?.ToString().ToLowerInvariant()} {Name}" : $"{Quantity} {Name}"; } else { return Name; } } public static Ingredient Parse(string text, bool metric) { var result = new Ingredient { Name = "" }; // If it starts with a number, treat that as the quantity text = text.Trim(); var leadingNumberMatch = Regex.Match(text, "^\\d+([\\.\\,]\\d+)?"); if (leadingNumberMatch.Success && double.TryParse(leadingNumberMatch.Value, out var quantity)) { result.Quantity = quantity; text = text.Substring(leadingNumberMatch.Value.Length).Trim(); // Now see if we can extract a known unit var nextWord = Regex.Match(text, "^[A-Za-z]+\\b"); if (nextWord.Success) { result.Unit = UnitFromText(nextWord.Value.ToLowerInvariant()); if (result.Unit.HasValue) { text = text.Substring(nextWord.Value.Length).Trim(); } } } // Whatever remains is the text result.Name = text; result.SetSystem(metric); return result; } private static IngredientUnit? UnitFromText(ReadOnlySpan text) { if (UnitFromTextCore(text) is { } result) { return result; } // Try without trailing 's' return text.EndsWith("s") ? UnitFromTextCore(text[0..^1]) : default; } private static IngredientUnit? UnitFromTextCore(ReadOnlySpan text) { return text switch { "g" => IngredientUnit.Grams, "gr" => IngredientUnit.Grams, "gram" => IngredientUnit.Grams, "kg" => IngredientUnit.Kilograms, "kilo" => IngredientUnit.Kilograms, "kilogram" => IngredientUnit.Kilograms, "ml" => IngredientUnit.Millileters, "millileter" => IngredientUnit.Millileters, "l" => IngredientUnit.Liters, "liter" => IngredientUnit.Liters, "litre" => IngredientUnit.Liters, "tsp" => IngredientUnit.Teaspoons, "teaspoon" => IngredientUnit.Teaspoons, "tbsp" => IngredientUnit.Tablespoons, "tblsp" => IngredientUnit.Tablespoons, "tablespoon" => IngredientUnit.Tablespoons, "oz" => IngredientUnit.Ounces, "ounce" => IngredientUnit.Ounces, "lb" => IngredientUnit.Pounds, "pound" => IngredientUnit.Pounds, "floz" => IngredientUnit.FluidOunces, "gal" => IngredientUnit.Gallons, "gallon" => IngredientUnit.Gallons, "c" => IngredientUnit.Cups, "cp" => IngredientUnit.Cups, "cup" => IngredientUnit.Cups, "bushel" => IngredientUnit.Bushels, "af" => IngredientUnit.AcreFeet, "acrefeet" => IngredientUnit.AcreFeet, "acrefoot" => IngredientUnit.AcreFeet, _ => null, }; } public void SetSystem(bool metric) { if (SetSystemExact(metric)) { Quantity = Math.Round(Quantity!.Value * 10) / 10.0; } } private bool SetSystemExact(bool metric) { if (!Unit.HasValue || IsMetricUnit(Unit.Value) == metric) { // No change required return false; } if (TryGetWeightInGrams(out var grams)) { if (metric) { if (grams < 50) { Quantity = grams; Unit = IngredientUnit.Grams; } else if (grams < Kg_to_g) { Quantity = 10 * Math.Round(grams / 10.0); Unit = IngredientUnit.Grams; } else { Quantity = grams / Kg_to_g; Unit = IngredientUnit.Kilograms; } } else { if (grams < Lb_to_g) { Quantity = grams / Oz_to_g; Unit = IngredientUnit.Ounces; } else { Quantity = grams / Lb_to_g; Unit = IngredientUnit.Pounds; } } return true; } else if (TryGetVolumeInMillileters(out var ml)) { if (metric) { if (ml < 50) { Quantity = ml; Unit = IngredientUnit.Millileters; } else if (ml < L_to_ml) { Quantity = 10 * Math.Round(ml / 10.0); Unit = IngredientUnit.Millileters; } else { Quantity = ml / L_to_ml; Unit = IngredientUnit.Liters; } } else { if (ml < Tbsp_to_ml) { Quantity = ml / Tsp_to_ml; Unit = IngredientUnit.Teaspoons; } else if (ml < Floz_to_ml) { Quantity = ml / Tbsp_to_ml; Unit = IngredientUnit.Tablespoons; } else if (ml < Cup_to_ml) { Quantity = ml / Floz_to_ml; Unit = IngredientUnit.FluidOunces; } else if (ml < Gal_to_ml) { Quantity = ml / Cup_to_ml; Unit = IngredientUnit.Cups; } else if (ml < Bushel_to_ml) { Quantity = ml / Gal_to_ml; Unit = IngredientUnit.Gallons; } else if (ml < AcreFoot_to_ml) { Quantity = ml / Bushel_to_ml; Unit = IngredientUnit.Bushels; } else { Quantity = ml / AcreFoot_to_ml; Unit = IngredientUnit.AcreFeet; } } return true; } return false; } const double L_to_ml = 1000; const double Tsp_to_ml = 4.92892; const double Tbsp_to_ml = 14.7868; const double Floz_to_ml = 29.5735; const double Gal_to_ml = 3785.41; const double Cup_to_ml = 236.588; const double Bushel_to_ml = 35239.1; const double AcreFoot_to_ml = 1.233e+9; const double Kg_to_g = 1000; const double Oz_to_g = 28.3495; const double Lb_to_g = 453.592; private bool TryGetWeightInGrams(out double grams) { if (Unit.HasValue && Quantity.HasValue) { switch (Unit) { case IngredientUnit.Grams: grams = Quantity.Value; return true; case IngredientUnit.Kilograms: grams = Quantity.Value * Kg_to_g; return true; case IngredientUnit.Ounces: grams = Quantity.Value * Oz_to_g; return true; case IngredientUnit.Pounds: grams = Quantity.Value * Lb_to_g; return true; } } grams = default; return false; } private bool TryGetVolumeInMillileters(out double ml) { if (Unit.HasValue && Quantity.HasValue) { switch (Unit) { case IngredientUnit.Millileters: ml = Quantity.Value; return true; case IngredientUnit.Liters: ml = Quantity.Value * L_to_ml; return true; case IngredientUnit.Teaspoons: ml = Quantity.Value * Tsp_to_ml; return true; case IngredientUnit.Tablespoons: ml = Quantity.Value * Tbsp_to_ml; return true; case IngredientUnit.FluidOunces: ml = Quantity.Value * Floz_to_ml; return true; case IngredientUnit.Gallons: ml = Quantity.Value * Gal_to_ml; return true; case IngredientUnit.Cups: ml = Quantity.Value * Cup_to_ml; return true; case IngredientUnit.Bushels: ml = Quantity.Value * Bushel_to_ml; return true; case IngredientUnit.AcreFeet: ml = Quantity.Value * AcreFoot_to_ml; return true; } } ml = default; return false; } private static bool IsMetricUnit(IngredientUnit unit) { switch (unit) { case IngredientUnit.Grams: case IngredientUnit.Kilograms: case IngredientUnit.Liters: case IngredientUnit.Millileters: return true; default: return false; } } } public enum IngredientUnit { Grams, Kilograms, Ounces, Pounds, Liters, Millileters, Teaspoons, Tablespoons, FluidOunces, Gallons, Cups, Bushels, AcreFeet, } ================================================ FILE: BestForYouRecipes.Client/Data/Recipe.cs ================================================ using StarRatings; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; namespace BestForYouRecipes; public class Recipe { public string Id { get; set; } = default!; [Required] public string Name { get; set; } = default!; public string Source { get; set; } = default!; [IgnoreDataMember] public string SourceShort => Uri.TryCreate(Source, UriKind.Absolute, out var sourceUri) ? sourceUri.Authority : Source; public int PrepTime { get; set; } public int WaitTime { get; set; } public int CookTime { get; set; } [Range(1, 100)] public int Servings { get; set; } public string? Comments { get; set; } public IList Reviews { get; set; } = new List(); [Required] public string Instructions { get; set; } = default!; [Required(ErrorMessage = "Please add your ingredients"), MinLength(1, ErrorMessage = "Please add your ingredients")] public string[] Ingredients { get; set; } = default!; public string[] Tags { get; set; } = default!; [Required(ErrorMessage = "Please add a picture")] public string? ImageUrl { get; set; } [IgnoreDataMember] public Uri CardImageUrl => new Uri(ImageUrl ?? $"images/cards/{Name}.png", UriKind.Relative); [IgnoreDataMember] public Uri BannerImageUrl => new Uri(ImageUrl ?? $"images/banners/{Name} Banner.png", UriKind.Relative); } ================================================ FILE: BestForYouRecipes.Client/Data/RecipesStore.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http.Json; namespace BestForYouRecipes.Client.Data; public class RecipesStore : IRecipesStore { private readonly HttpClient _http; public RecipesStore(HttpClient http) { _http = http; } public async Task AddImage(Stream imageData) { var response = await _http.PostAsync("api/images", new StreamContent(imageData)); return await response.Content.ReadAsStringAsync(); } public async Task AddRecipe(Recipe recipe) { var response = await _http.PostAsJsonAsync("api/recipes", recipe); return await response.Content.ReadAsStringAsync(); } public Task> GetRecipes(string? query) => throw new NotImplementedException(); public Task GetRecipe(string id) => throw new NotImplementedException(); public Task UpdateRecipe(Recipe recipe) => throw new NotImplementedException(); public Task DownloadImage(string filename, Stream stream) => throw new NotImplementedException(); } ================================================ FILE: BestForYouRecipes.Client/IngredientsListEditor.razor ================================================ @inject IJSRuntime JS @implements IAsyncDisposable
Units:
@foreach (var ingredient in ingredients) {
@if (metric) { } else { }
}
@code { string? newIngredientText; ElementReference ingredientsElem; List ingredients = new(); bool metric = true; IJSObjectReference? jsModule; DotNetObjectReference? selfReference; string onEnterClickNextButton = "if (event.code === 'Enter') { event.preventDefault(); event.target.dispatchEvent(new Event('change')); event.target.nextElementSibling.click(); }"; [CascadingParameter] public EditContext? EditContext { get; set; } [Parameter] public string[] Ingredients { get; set; } = default!; [Parameter] public EventCallback IngredientsChanged { get; set; } = default!; [Parameter] public Expression> IngredientsExpression { get; set; } = default!; async Task UpdateSystem() { foreach (var i in ingredients) { i.SetSystem(metric); } await UpdateIngredientsOnModel(); } async Task AddIngredient() { if (!string.IsNullOrWhiteSpace(newIngredientText)) { ingredients.Add(Ingredient.Parse(newIngredientText, metric)); } newIngredientText = null; await UpdateIngredientsOnModel(); } async Task RemoveIngredient(Ingredient ingredient) { ingredients.Remove(ingredient); await UpdateIngredientsOnModel(); } async Task UpdateIngredientsOnModel() { var result = ingredients.Select(i => i.ToString()).ToArray(); await IngredientsChanged.InvokeAsync(result); EditContext?.NotifyFieldChanged(FieldIdentifier.Create(IngredientsExpression)); } [JSInvokable] public async Task ChangeIngredientsOrder(int oldIndex, int newIndex) { var item = ingredients[oldIndex]; ingredients.RemoveAt(oldIndex); ingredients.Insert(newIndex, item); await UpdateIngredientsOnModel(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { jsModule = await JS.InvokeAsync("import", "./IngredientsListEditor.razor.js"); selfReference = DotNetObjectReference.Create(this); await jsModule.InvokeVoidAsync("enableDragging", ingredientsElem, selfReference); } } public async ValueTask DisposeAsync() { selfReference?.Dispose(); if (jsModule is not null) { try { await jsModule.DisposeAsync(); } catch (JSDisconnectedException) { // If disconnected, then there's no reason why the server should be concerned about disposing IJSObjectReference instances. // https://github.com/dotnet/aspnetcore/issues/49418#issuecomment-1636309956 } } } } ================================================ FILE: BestForYouRecipes.Client/IngredientsListEditor.razor.css ================================================ .units-group { border: 1px solid #ddd; display: inline-block; padding: 0.6rem 1rem; margin-bottom: 1rem; border-radius: 6px; } .ingredient { padding: 0.3rem; display: flex; gap: .5rem; } .ingredient:last-of-type { margin-bottom: 1rem; } .ingredient button { background: none; color: black; width: 2.5rem; padding: 0; cursor: pointer; margin-right: -0.5rem; } .ingredient button:hover { background: #ddd; } .ingredient button:active { background: #ccc; } .ingredient .draghandle { cursor: move; } ================================================ FILE: BestForYouRecipes.Client/IngredientsListEditor.razor.js ================================================ import '/Sortable.min.js'; export function enableDragging(container, componentInstance) { Sortable.create(container, { handle: '.draghandle', onEnd: evt => { // Preserve the original DOM until the component re-renders const offset = evt.oldIndex > evt.newIndex ? 1 : 0; evt.from.insertBefore(evt.item, container.children[evt.oldIndex + offset]); componentInstance.invokeMethodAsync('ChangeIngredientsOrder', evt.oldIndex, evt.newIndex); } }); } ================================================ FILE: BestForYouRecipes.Client/Pages/SubmitRecipe.razor ================================================ @page "/submit" @inject IRecipesStore RecipesStore @inject NavigationManager Nav @inject IJSRuntime JS @rendermode InteractiveAuto

Recipe Editor

Share your great recipes with the Best For You community.

Title

Picture

Number of servings

Ingredients

Instructions

Tags

TODO: Edit tags

@code { private InputFile? inputFile; private ElementReference previewImageElem; EditForm? editForm; [SupplyParameterFromForm] public Recipe recipe { get; set; } = new Recipe { Servings = 2, Source = "Best For You community", Ingredients = Array.Empty() }; private async Task ImageSelected(InputFileChangeEventArgs eventArgs) { await using var jsModule = await JS.InvokeAsync("import", "./Pages/SubmitRecipe.razor.js"); await jsModule!.InvokeVoidAsync("previewImage", inputFile!.Element, previewImageElem); var image = await eventArgs.File.RequestImageFileAsync("jpeg", 800, 600); await using var stream = image.OpenReadStream(2 * 1024 * 1024); recipe.ImageUrl = await RecipesStore.AddImage(stream); editForm!.EditContext!.NotifyFieldChanged(FieldIdentifier.Create(() => recipe.ImageUrl)); } async Task HandleSubmit() { recipe.Tags = new[] { "TODO" }; recipe.Id = await RecipesStore.AddRecipe(recipe); // In a real application, this would probably go through some kind of administrative review process Nav.NavigateTo($"recipe/{recipe.Id}"); } } ================================================ FILE: BestForYouRecipes.Client/Pages/SubmitRecipe.razor.css ================================================ ::deep input:not([type]), ::deep input[type=text], ::deep input[type=number], ::deep textarea { width: 20rem; padding: 0.3rem 0.6rem; } ::deep input::placeholder { color: #ccc; } button { padding: 0.4rem 2rem; font-size: 1.05rem; } h2 { margin-top: 1.5rem; margin-bottom: 1rem; } ::deep .validation-message { margin-top: 0.5rem; } ::deep input[type=number] { width: 3rem; text-align: center; } img.preview { display: block; max-width: 30rem; margin-top: 1rem; } ================================================ FILE: BestForYouRecipes.Client/Pages/SubmitRecipe.razor.js ================================================ export function previewImage(inputElem, imgElem) { const url = URL.createObjectURL(inputElem.files[0]); imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true }); imgElem.src = url; } ================================================ FILE: BestForYouRecipes.Client/Program.cs ================================================ using BestForYouRecipes; using BestForYouRecipes.Client.Data; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(); await builder.Build().RunAsync(); ================================================ FILE: BestForYouRecipes.Client/Properties/launchSettings.json ================================================ { "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:5595", "sslPort": 44395 } }, "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5030", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:7046;http://localhost:5030", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: BestForYouRecipes.Client/RecipeEditor.razor ================================================  ================================================ FILE: BestForYouRecipes.Client/_Imports.razor ================================================ @using System.Linq.Expressions @using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop @using BestForYouRecipes @namespace BestForYouRecipes ================================================ FILE: BestForYouRecipes.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.33906.173 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BestForYouRecipes", "BestForYouRecipes\BestForYouRecipes.csproj", "{B37AD23A-E310-4A60-A336-B93C14422891}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BestForYouRecipes.Client", "BestForYouRecipes.Client\BestForYouRecipes.Client.csproj", "{467F286B-80DF-4987-BF0F-03741E4089B2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarRatings", "StarRatings\StarRatings.csproj", "{AC5EB257-E319-4A92-813A-7003E6FF3679}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B37AD23A-E310-4A60-A336-B93C14422891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B37AD23A-E310-4A60-A336-B93C14422891}.Debug|Any CPU.Build.0 = Debug|Any CPU {B37AD23A-E310-4A60-A336-B93C14422891}.Release|Any CPU.ActiveCfg = Release|Any CPU {B37AD23A-E310-4A60-A336-B93C14422891}.Release|Any CPU.Build.0 = Release|Any CPU {467F286B-80DF-4987-BF0F-03741E4089B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {467F286B-80DF-4987-BF0F-03741E4089B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {467F286B-80DF-4987-BF0F-03741E4089B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {467F286B-80DF-4987-BF0F-03741E4089B2}.Release|Any CPU.Build.0 = Release|Any CPU {AC5EB257-E319-4A92-813A-7003E6FF3679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC5EB257-E319-4A92-813A-7003E6FF3679}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC5EB257-E319-4A92-813A-7003E6FF3679}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC5EB257-E319-4A92-813A-7003E6FF3679}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0E67C23E-69A0-4803-9841-AABA02185A79} EndGlobalSection EndGlobal ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Daniel Roth Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Best For You recipes Best For You recipes site in Blazor. - Search with debouncing - Reusable star ratings components ![image](https://user-images.githubusercontent.com/1874516/72215097-78500a80-34c3-11ea-9141-f610dc15604c.png) ================================================ FILE: StarRatings/Review.cs ================================================ using System.ComponentModel.DataAnnotations; namespace StarRatings; public class Review { [Required] [Range(1, 5)] public double Rating { get; set; } [Required] [StringLength(50, ErrorMessage = "Text must be no more than 50 characters.")] public string? Text { get; set; } } ================================================ FILE: StarRatings/ReviewExtensions.cs ================================================ namespace StarRatings; public static class ReviewExtensions { public static double AverageRating(this IList reviews) => Math.Round(reviews.Select(review => review.Rating).DefaultIfEmpty(0).Average(), 1); } ================================================ FILE: StarRatings/StarRating.razor ================================================ 
@if (!ValueChanged.HasDelegate) { ☆☆☆☆☆ @Math.Round(Value, 1) } else { for (int i = 5; i > 0; i--) { var id = $"star{i}"; var value = i; } }
@code { FieldIdentifier fieldIdentifier; protected override void OnInitialized() { if (ValueExpression != null) { fieldIdentifier = FieldIdentifier.Create(ValueExpression); } } [CascadingParameter] EditContext? EditContext { get; set; } [Parameter] public double Value { get; set; } [Parameter] public EventCallback ValueChanged { get; set; } [Parameter] public Expression>? ValueExpression { get; set; } string CssClass => $"star-rating {EditContext?.FieldCssClass(fieldIdentifier) ?? ""}"; async Task OnChange(ChangeEventArgs args) { Value = double.Parse(args.Value!.ToString()!); await ValueChanged.InvokeAsync(Value); EditContext?.NotifyFieldChanged(fieldIdentifier); } } ================================================ FILE: StarRatings/StarRating.razor.css ================================================ .star-rating { --percent: calc(var(--star-rating) / 5 * 100%); font-size: 1.5em; display: inline-block; } .star-rating .stars { position: relative; display: inline-block; } .star-rating .stars::before { position: absolute; content: "☆☆☆☆☆"; color: #666666; } .star-rating .stars::after { position: absolute; top: 0; left: 0; content: "★★★★★"; color: #feac25; width: var(--percent); overflow: hidden; } .star-rating .rating { font-size: 1ex; color: #605E5C } .star-rating input { display: none; } .star-rating label { float: right; } .star-rating label:before { content: "☆"; color: #666666; cursor: pointer; } .star-rating input:checked ~ label:before, .star-rating label:hover:before, .star-rating label:hover ~ label:before { content: "★"; color: #feac25; } ================================================ FILE: StarRatings/StarRatingReviews.razor ================================================ @inject NavigationManager NavigationManager

Reviews

@for (int i = 0; i < Reviews.Count; i++) { var review = Reviews[i];

@review.Text

@if (i < Reviews.Count - 1) {
}
}
@code { Review review = new(); [SupplyParameterFromForm] public Review Review { get; set; } = new(); [Parameter, EditorRequired] public IList Reviews { get; set; } = default!; [Parameter] public EventCallback OnSubmitReview { get; set; } protected override void OnInitialized() { review = Review; } async Task OnValidSubmit() { await OnSubmitReview.InvokeAsync(review); NavigationManager.NavigateTo(NavigationManager.Uri); } } ================================================ FILE: StarRatings/StarRatingReviews.razor.css ================================================ .reviews ::deep textarea { display: block; padding: 6px 8px; margin-bottom: 5px; width: 100%; border: 1px solid #605E5C; border-radius: 2px; font-family: Open Sans, sans-serif; font-size: 0.8em; color: #605E5C; box-sizing: border-box; } .reviews ::deep .rating { display: none; } .review-editor { margin-bottom: 10px; } button { padding: 6px 25px; font-family: Playfair Display; color: white; background-color: #435869; border-radius: 2px; border: 0; } button:hover { background-color: #334452; } button:active { background-color: #26343f; } ::deep input.invalid, ::deep textarea.invalid, ::deep div.invalid { border: 1px solid red; } ::deep .validation-errors { color: red; font-size: 0.8em; } ================================================ FILE: StarRatings/StarRatings.csproj ================================================  net8.0 enable enable ================================================ FILE: StarRatings/_Imports.razor ================================================ @using System.Linq.Expressions @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Forms;