*@
26 |
27 |
28 |
29 |
The Random Number is..
30 | @* TODO: Add hx-trigger="sse:number" and hx-get="@Url.Page("11_ServerEvents", "Random")" to following
*@
31 |
32 | @Number.Value
33 |
34 |
35 |
36 |
37 |
38 | @* Complete Solution *@
39 | @*
40 |
41 |
42 |
43 |
44 |
The Random Number is..
45 |
48 | @Number.Value
49 |
50 |
51 |
52 |
53 | *@
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/11_ServerEvents.cshtml.cs:
--------------------------------------------------------------------------------
1 | using Exercises.Models;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.AspNetCore.Mvc.RazorPages;
4 |
5 | namespace Exercises.Pages
6 | {
7 | ///
8 | /// We will be receiving events from the ServerEventsWorker service
9 | /// about the need to update our UI. We've registered the worker
10 | /// as a hosted service in our startup file (Program.cs).
11 | ///
12 | ///
13 | [ResponseCache(Duration = 1 /* second */)]
14 | public class ServerEvents : PageModel
15 | {
16 | public void OnGet()
17 | {
18 | }
19 |
20 | public ContentResult OnGetRandom()
21 | {
22 | return Content($"
{Number.Value} ", "text/html");
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/12_TagHelpers.cshtml:
--------------------------------------------------------------------------------
1 | @page "/examples/12-taghelpers"
2 | @model Exercises.Pages.TagHelpers
3 |
4 | @{
5 | ViewBag.Title = "Tag Helpers";
6 | ViewBag.Previous = "11_ServerEvents";
7 | ViewBag.Next = "13_ClientsideTemplates";
8 | }
9 |
10 |
11 | # 12. ASP.NET Core Tag Helpers
12 |
13 | During your journey through the different samples, you've likely noticed the use of `Url.Page` to generate links to each Razor page. While the approach works, it can be noisy and tedious. Instead, you can use the tag helpers found in **Htmx.TagHelpers** to generate links to any ASP.NET Core endpoint. HTMX.NET models the tag helpers after the `asp-` tag helpers found in ASP.NET Core so that you can create links to endpoints, MVC actions, and Razor Pages. Let's look at an example.
14 |
15 | ```
16 | hx-get
17 | hx-page="12_TagHelpers"
18 | hx-target="#target"
19 | ```
20 |
21 | In this case, you need to specify the HTTP method version of the `hx` attribute, and then you can use `hx-page` to point to our Razor Page. Using these tag helpers can help you keep our Razor views clean and concise.
22 |
23 | ---
24 |
25 |
26 |
27 |
Change Me!
28 |
29 |
30 |
31 |
32 | Click Me
33 |
34 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/12_TagHelpers.cshtml.cs:
--------------------------------------------------------------------------------
1 | using Htmx;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.AspNetCore.Mvc.RazorPages;
4 |
5 | namespace Exercises.Pages
6 | {
7 | public class TagHelpers : PageModel
8 | {
9 | public IActionResult OnGet()
10 | {
11 | return Request.IsHtmx()
12 | ? Content("
Hello, World with Tag Helpers! ", "text/html")
13 | : Page();
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/13_ClientsideTemplates.cshtml:
--------------------------------------------------------------------------------
1 | @page "/examples/13-clientside-templates"
2 | @model Exercises.Pages.ClientsideTemplates
3 |
4 | @{
5 | ViewBag.Title = "Clientside Templates";
6 | ViewBag.Previous = "12_ServerEvents";
7 | ViewBag.Next = "14_OutOfBand";
8 | }
9 |
10 | @section end{
11 |
12 |
13 | }
14 |
15 |
16 | # 13. Clientside Templates
17 |
18 | There are many ways development teams may work on building client-side experiences. For example, you may have chosen to start by making a web application first, focusing on HTML and HTTP interactions. On the other hand, you may have also started by building JSON APIs and investing significantly there. If you find yourself in the latter camp, there's still an opportunity to utilize HTMX.
19 |
20 | HTMX supports extensions, one of those being [**clientside templates**](https://htmx.org/extensions/client-side-templates/), with support for templating languages like **Mustache**, **Handlebars**, and **Nunjucks**. You will need additional dependencies such as the HTMX extension, along with the templating library of your choice. For instance, in this sample, you'll be using **Mustache**.
21 |
22 | The extension allows you to make HTMX calls to API endpoints known to return JSON and only JSON. Before injecting the response, HTMX will process the JSON through an HTML template found on the page. You'll need to use the `hx-ext` to enable the extension of the clientside template, along with the `template` tag to define our template.
23 |
24 | ```html
25 |
26 |
27 |
Change Me!
28 |
29 |
30 |
31 |
32 |
38 | Click Me
39 |
40 |
41 |
42 | The Id is #{{id}} with the author, {{author}}, writing "{{title}}".
43 |
44 |
45 | ```
46 |
47 | Now you can continue to build an immersive clientside experience while also developing your JSON API.
48 |
49 |
50 |
51 |
52 |
53 |
Change Me!
54 |
55 |
56 | @* TODO: Replace this button with the code above *@
57 |
58 | Click Me
59 |
60 |
61 |
62 | The Id is #{{id}} with the author, {{author}}, writing "{{title}}".
63 |
64 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/13_ClientsideTemplates.cshtml.cs:
--------------------------------------------------------------------------------
1 | using Htmx;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.AspNetCore.Mvc.RazorPages;
4 |
5 | namespace Exercises.Pages
6 | {
7 | public class ClientsideTemplates : PageModel
8 | {
9 | public void OnGet()
10 | {
11 | }
12 |
13 | public IActionResult OnGetJson()
14 | {
15 | return new JsonResult(new {
16 | Id = 1,
17 | Title = "This is Great!",
18 | Author = "You"
19 | });
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/14_OutOfBand.cshtml:
--------------------------------------------------------------------------------
1 | @page "/examples/13-out-of-band-swaps"
2 | @model Exercises.Pages.OutOfBand
3 |
4 | @{
5 | ViewBag.Title = "Out-of-Band Swaps";
6 | ViewBag.Previous = "13_ClientsideTemplates";
7 | ViewBag.Home = true;
8 | }
9 |
10 |
11 | # 14. Out of Band Swaps
12 |
13 | Very often, with HTMX, you'll be replacing relatively local elements with the results of your action. For example, you'll submit a form and then replace that form with a notification. It's a pattern that will likely be the majority of your experience with HTMX. That said, there are a few circumstances where you may want to update other global elements through an interaction.
14 |
15 | An everyday use case might be a global shopping cart indicator in the navigation of your site. Adding or removing items will update a local count for the product and a global cart item count.
16 |
17 | HTMX comes with an **[Out of Band Swaps](https://htmx.org/docs/#oob_swaps)** feature which allows you to piggyback additional UI elements on the response from your original interaction. For example, on the response, we can add the `hx-swap-oob` attribute on any element with an `id`, with htmx finding and replacing the existing component on the page with the new response. The rest of the HTML response will follow the default HTMX behavior.
18 |
19 | ```html
20 | Swap me directly!
21 | Additional Content
22 | ```
23 |
24 | Check out the sample below for a snappy shopping experience.
25 |
26 |
27 |
28 |
29 |
30 |
41 |
42 | @* TODO: Look at Shared/_ShoppingItem *@
43 | @await Html.PartialAsync("_ShoppingItem")
44 |
45 |
46 | @section end {
47 |
48 | }
49 |
50 | @section head {
51 |
91 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/14_OutOfBand.cshtml.cs:
--------------------------------------------------------------------------------
1 | using Htmx;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.AspNetCore.Mvc.RazorPages;
4 |
5 | namespace Exercises.Pages
6 | {
7 | public class OutOfBand : PageModel
8 | {
9 | public int TotalItemsInCart { get; set; } = 1;
10 | public bool RenderCartOutOfBand { get; set; }
11 |
12 | public void OnGet()
13 | {
14 | }
15 |
16 | public IActionResult OnPostAddToCart(int count)
17 | {
18 |
19 |
20 |
21 | TotalItemsInCart = count;
22 | RenderCartOutOfBand = true;
23 | return Partial("_ShoppingItem", this);
24 | }
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Index.cshtml:
--------------------------------------------------------------------------------
1 | @page "/"
2 | @model Exercises.End.Pages.Index
3 | @{
4 | ViewBag.Title = "Home";
5 | }
6 |
7 |
8 | # Getting Started with HTMX For ASP.NET Core Developers
9 | ## by Khalid Abuhakmeh
10 |
11 | ---
12 |
13 | **_Hello, [ASP.NET Core](https://dot.net) developers!_**
14 |
15 | If you're running this solution, you're likely following along with the video series from JetBrains, or you're very interested in [HTMX](https://htmx.org) as a solution to your frontend problems.
16 |
17 | Let's start by describing what HTMX is for folks new to the library. HTMX is a JavaScript library that performs HTTP requests and relies heavily on the server stack's ability to deliver HTML fragments. HTMX then processes those responses into an existing page. Developers can decorate any HTML element with HTMX attributes that pair with HTTP methods: `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`.
18 |
19 | To understand what HTMX is, we also should think about what it is not. It is not a complicated framework approach to building client-side user experiences. It does not require build or pack tools, and it does not require an "all-in" approach, where you need to write every part of your front end using HTMX. Instead, the library gives you the freedom to enhance your user experience when you choose to do so. In fact, to install HTMX, all you need is a pre-minized JavaScript file in your HTML pages. That's it!
20 |
21 | As you may already know, ASP.NET Core is a powerful server-side technology and pairs well with the HTMX philosophy. This solution will show you how you can pair the two technologies together to deliver fantastic user experiences with a few lines of code.
22 |
23 | **_Let's Get Started!_**
24 |
25 | ## Exercises
26 |
27 | Be sure to look at the code to see how it all comes together.
28 |
29 | 1. Hello World
30 | 1. Counter
31 | 1. Select (Dropdowns)
32 | 1. Search
33 | 1. Scroll
34 | 1. Modal
35 | 1. Tabs
36 | 1. Shortcuts
37 | 1. Form Validation
38 | 1. Polling
39 | 1. Server Sent Events
40 | 1. ASP.NET Core Tag Helpers
41 | 1. Clientside Templates & JSON APIs
42 | 1. Out of Band Swaps
43 |
44 | ## Related Links
45 |
46 | This section includes links to related content you may want to be familiar with.
47 |
48 | - [JetBrains .NET Guide](https://www.jetbrains.com/dotnet/guide/)
49 | - [HTMX Documentation](https://htmx.org)
50 | - [.NET SDK download](https://dotnet.microsoft.com/download)
51 | - [HTMX.NET](https://github.com/khalidabuhakmeh/htmx.net)
52 | - [Simple, Fast Frontends with HTMX](https://www.youtube.com/watch?v=cBfz4W_KvEI)
53 | - [HTMX Twitter](https://twitter.com/htmx_org)
54 | - [JetBrains dotUltimate](https://www.jetbrains.com/dotnet/)
55 |
56 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Index.cshtml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.RazorPages;
2 |
3 | namespace Exercises.End.Pages
4 | {
5 | public class Index : PageModel
6 | {
7 | public void OnGet()
8 | {
9 |
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/Pagination.cshtml:
--------------------------------------------------------------------------------
1 | @using System.Text.RegularExpressions
2 | @{
3 | string Name(string page)
4 | {
5 | var name = page.Split("_", StringSplitOptions.TrimEntries)[^1];
6 | return Regex.Replace(name, "([A-Z])", " $1", RegexOptions.Compiled).Trim();
7 | }
8 | }
9 |
10 | @if (ViewBag.Previous is string previous)
11 | {
12 |
13 |
14 | @Name(previous)
15 |
16 | }
17 | @if (ViewBag.Home is true)
18 | {
19 |
20 |
21 | Home
22 |
23 | }
24 | @if (ViewBag.Next is string next)
25 | {
26 |
27 | @Name(next)
28 |
29 |
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_Cards.cshtml:
--------------------------------------------------------------------------------
1 | @model Exercises.Pages.Scroll
2 | @{
3 | ArgumentNullException.ThrowIfNull(Model);
4 | var end = Model.Cursor + 19;
5 | }
6 |
7 | @for (var i = Model.Cursor; i <= end; i++)
8 | {
9 |
*@
14 | @* hx-get="@Url.Page("05_Scroll", new {cursor = end + 1})" *@
15 | @* hx-trigger="revealed" *@
16 | @* hx-swap="afterend" *@
17 | @* *@
18 | @* } *@
19 | class="card mb-4 ms-1" aria-hidden="true">
20 |
24 |
25 |
26 | Card #@i
27 |
28 |
29 |
30 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_Form.cshtml:
--------------------------------------------------------------------------------
1 | @model Exercises.Pages.FormValidation
2 |
3 | @* TODO: Replace the following
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @ViewData["Title"] - HTMX for ASP.NET Core Developers Samples
7 |
8 |
9 |
10 | @await RenderSectionAsync("head", false)
11 |
12 |
13 |
14 |
15 |
21 |
22 |
23 | @RenderBody()
24 | @await Html.PartialAsync("Pagination")
25 |
26 |
27 |
28 | Created by the JetBrains team · © 2021
29 |
30 |
31 |
32 |
33 |
34 |
35 | @await RenderSectionAsync("end", false)
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_Modal.cshtml:
--------------------------------------------------------------------------------
1 | @model Exercises.Pages.NewsletterSignup
2 | @{ ArgumentNullException.ThrowIfNull(Model); }
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 | Thanks for subscribing to our AMAZING NEWSLETTER!
14 |
15 |
16 |
Expect amazing things to appear in your inbox at @Model.Email
.
17 |
18 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_Results.cshtml:
--------------------------------------------------------------------------------
1 | @model Exercises.Pages.Search
2 | @{ ArgumentNullException.ThrowIfNull(Model); }
3 |
4 | @if (Model.Results is {} games)
5 | {
6 | @foreach (var game in games)
7 | {
8 |
9 | @game.Year
10 | @game.Publisher
11 | @game.Console
12 | @game.Name
13 |
14 | }
15 | }
16 | else
17 | {
18 |
19 | No Results for "@Model.Query"
20 |
21 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_ShoppingItem.cshtml:
--------------------------------------------------------------------------------
1 | @model Exercises.Pages.OutOfBand
2 | @{ ArgumentNullException.ThrowIfNull(Model); }
3 |
4 | @if (Model.RenderCartOutOfBand)
5 | {
6 | @* TODO: Note the use of hx-swap-oob and id attributes *@
7 |
8 |
9 | Cart
10 | @Model.TotalItemsInCart
11 |
12 | }
13 |
14 |
15 |
16 |
Organics by LaMana
17 |
18 |
19 |
20 |
Pineapple Green Packham Each - approx 1.2KG
21 |
22 |
23 | @* TODO: Enhance the form below with HTMX attributes *@
24 | @*
25 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_Stonks.cshtml:
--------------------------------------------------------------------------------
1 | @model Exercises.Pages.Polling
2 | @{ ArgumentNullException.ThrowIfNull(Model); }
3 |
4 |
5 | @foreach (var co in Model.Companies)
6 | {
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 | @if (co.IsUp)
17 | {
18 |
19 |
20 | @co.CurrentPrice.ToString("$#.00")
21 |
22 | }
23 | else
24 | {
25 |
26 |
27 |
28 |
29 | @co.CurrentPrice.ToString("$#.00")
30 |
31 | }
32 |
33 |
34 | Opened @@ @co.OpeningPrice.ToString("$#.00")
35 |
36 |
37 |
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_Tabs.cshtml:
--------------------------------------------------------------------------------
1 | @model Exercises.Pages.Tabs
2 | @{ ArgumentNullException.ThrowIfNull(Model); }
3 |
4 |
5 |
6 | @foreach (var tab in Model.Items)
7 | {
8 |
9 |
10 | @tab
11 |
12 |
13 |
14 | @* TODO: alter above anchor with HTMX attributes and ASP.NET Core helpers
15 |
16 |
22 | @tab
23 |
24 | *@
25 |
26 | }
27 |
28 |
29 |
30 |
31 |
@Model.Tab
32 |
33 |
39 |
40 | Commodo normcore truffaut VHS duis gluten-free keffiyeh iPhone taxidermy godard ramps anim pour-over.
41 | Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore.
42 | Polaroid duis occaecat narwhal small batch food truck.
43 | PBR & B venmo shaman small batch you probably haven't heard of them hot chicken readymade.
44 | Enim tousled cliche woke, typewriter single-origin coffee hella culpa.
45 | Art party readymade 90's, asymmetrical hell of fingerstache ipsum.
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/Shared/_Toast.cshtml:
--------------------------------------------------------------------------------
1 | @model Exercises.Pages.Shortcuts.Quote
2 | @{ ArgumentNullException.ThrowIfNull(Model); }
3 |
4 |
5 |
15 |
16 |
Hello!
17 |
How Can I Help? You seem to need some assitance.
18 |
19 |
20 |
21 | @Model.Text
22 |
23 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using Exercises.Models
2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
3 | @addTagHelper *, Westwind.AspNetCore.Markdown
4 | @addTagHelper *, Htmx.TagHelpers
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Pages/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "Shared/_Layout";
3 | }
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Program.cs:
--------------------------------------------------------------------------------
1 | using Exercises.Models;
2 | using Lib.AspNetCore.ServerSentEvents;
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | var builder = WebApplication.CreateBuilder(args);
6 |
7 | // add dependencies to services collection
8 | builder.Services.AddHttpClient();
9 | builder.Services.AddRazorPages(o => {
10 | // this is to make demos easier
11 | // don't do this in production
12 | o.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
13 | }).AddRazorRuntimeCompilation();
14 |
15 | // dependencies for server sent events
16 | // Lib.AspNetCore.ServerSentEvents
17 | builder.Services.AddServerSentEvents();
18 | builder.Services.AddHostedService
();
19 |
20 | // define asp.net request pipeline
21 | var app = builder.Build();
22 | app.UseHttpsRedirection();
23 | app.UseDefaultFiles();
24 | app.UseStaticFiles();
25 |
26 | // the connection for server events
27 | app.MapServerSentEvents("/rn-updates");
28 | app.MapRazorPages();
29 |
30 | app.Run();
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Exercises.Start": {
4 | "commandName": "Project",
5 | "dotnetRunMessages": true,
6 | "launchBrowser": true,
7 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
8 | "environmentVariables": {
9 | "ASPNETCORE_ENVIRONMENT": "Development"
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-brands-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-brands-400.eot
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-brands-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-brands-400.woff
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-regular-400.eot
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-regular-400.woff
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/css/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/img/Pacman.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/img/Pacman.gif
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/img/bars.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
12 |
13 |
17 |
21 |
22 |
23 |
27 |
31 |
32 |
33 |
37 |
41 |
42 |
43 |
47 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/img/jetbrains.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/Exercises/Exercises.Start/wwwroot/img/jetbrains.png
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/img/joystick.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/img/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/Exercises/Exercises.Start/wwwroot/js/extra/client-side-templates.js:
--------------------------------------------------------------------------------
1 | htmx.defineExtension('client-side-templates', {
2 | transformResponse : function(text, xhr, elt) {
3 |
4 | var mustacheTemplate = htmx.closest(elt, "[mustache-template]");
5 | if (mustacheTemplate) {
6 | var data = JSON.parse(text);
7 | var templateId = mustacheTemplate.getAttribute('mustache-template');
8 | var template = htmx.find("#" + templateId);
9 | if (template) {
10 | return Mustache.render(template.innerHTML, data);
11 | } else {
12 | throw "Unknown mustache template: " + templateId;
13 | }
14 | }
15 |
16 | var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
17 | if (handlebarsTemplate) {
18 | var data = JSON.parse(text);
19 | var templateName = handlebarsTemplate.getAttribute('handlebars-template');
20 | return Handlebars.partials[templateName](data);
21 | }
22 |
23 | var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
24 | if (nunjucksTemplate) {
25 | var data = JSON.parse(text);
26 | var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
27 | var template = htmx.find('#' + templateName);
28 | if (template) {
29 | return nunjucks.renderString(template.innerHTML, data);
30 | } else {
31 | return nunjucks.render(templateName, data);
32 | }
33 | }
34 |
35 | return text;
36 | }
37 | });
38 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/Category.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models;
2 |
3 | public class Category
4 | {
5 | public int Id { get; set; }
6 | public string Name { get; set; } = "";
7 | public int Sort { get; set; } = 10_000;
8 |
9 | public ICollection Products { get; set; }
10 | = new List();
11 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/Extensions/QueryableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 |
3 | namespace JetSwagStore.Models.Extensions;
4 |
5 | public static class QueryableExtensions
6 | {
7 | public static IQueryable If(
8 | this IQueryable query,
9 | bool should,
10 | params Func, IQueryable>[] transforms)
11 | {
12 | return should
13 | ? transforms.Aggregate(query,
14 | (current, transform) => transform.Invoke(current))
15 | : query;
16 | }
17 |
18 | public static IEnumerable If(
19 | this IEnumerable query,
20 | bool should,
21 | params Func, IEnumerable>[] transforms)
22 | {
23 | return should
24 | ? transforms.Aggregate(query,
25 | (current, transform) => transform.Invoke(current))
26 | : query;
27 | }
28 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/JetSwagStore.Models.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 | all
12 | runtime; build; native; contentfiles; analyzers; buildtransitive
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/Order.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models;
2 |
3 | public class Order
4 | {
5 | public int Id { get; set; }
6 | public string Code { get; set; } = "";
7 | public string CustomerEmail { get; set; } = "";
8 | public string ExternalPaymentId { get; set; } = "";
9 |
10 | public DateTime PurchasedAt { get; set; }
11 | = DateTime.UtcNow;
12 |
13 | public string Status { get; set; }
14 | = OrderStatuses.Submitted;
15 |
16 | public string ShippingStreet { get; set; } = "";
17 | public string ShippingCity { get; set; } = "";
18 | public string ShippingStateOrProvince { get; set; } = "";
19 | public string ShippingPostalCode { get; set; } = "";
20 | public string ShippingCountry { get; set; } = "";
21 |
22 | public ICollection Items { get; set; }
23 | = new List();
24 | }
25 |
26 | public static class OrderStatuses
27 | {
28 | public const string Submitted = nameof(Submitted);
29 | public const string Processing = nameof(Processing);
30 | public const string Shipped = nameof(Shipped);
31 | public const string Delivered = nameof(Delivered);
32 | public const string Canceled = nameof(Canceled);
33 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/OrderItem.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models;
2 |
3 | public class OrderItem
4 | {
5 | public int Id { get; set; }
6 | public string Name { get; set; } = "";
7 | public string Description { get; set; } = "";
8 | public string Manufacturer { get; set; } = "";
9 | public string ProductOptionName { get; set; } = "";
10 |
11 | public double Price { get; set; }
12 | public int Amount { get; set; }
13 |
14 | public int OrderId { get; set; }
15 | public Order Order { get; set; } = null!;
16 |
17 | // products can eventually disappear
18 | public int? ProductId { get; set; }
19 | public Product? Product { get; set; }
20 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/Product.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models;
2 |
3 | public class Product
4 | {
5 | public int Id { get; set; }
6 | public string Name { get; set; } = "";
7 | public string Description { get; set; } = "";
8 | public string Manufacturer { get; set; } = "";
9 | public string ImageUrl { get; set; } = "";
10 | public double Price { get; set; }
11 | public double? DiscountPrice { get; set; }
12 | public int? CurrentInventory { get; set; }
13 |
14 | public ICollection Options { get; set; }
15 | = new List();
16 |
17 | public ICollection Categories { get; set; }
18 | = new List();
19 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/ProductOption.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations.Schema;
2 |
3 | namespace JetSwagStore.Models;
4 |
5 | public class ProductOption
6 | {
7 | public int Id { get; set; }
8 | public string Name { get; set; } = "";
9 | public double AdditionalCost { get; set; }
10 | public int CurrentInventory { get; set; } = 1;
11 | public int ProductId { get; set; }
12 |
13 | public Product Product { get; set; } = null!;
14 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/ShoppingCart.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models;
2 |
3 | public class ShoppingCart
4 | {
5 | public int Id { get; set; }
6 | public DateTime CreatedAt { get; set; }
7 | = DateTime.UtcNow;
8 | public ICollection Items { get; set; }
9 | = new List();
10 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Models/ShoppingCartItem.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models;
2 |
3 | public class ShoppingCartItem
4 | {
5 | public int Id { get; set; }
6 | public int Quantity { get; set; }
7 | public int? ProductId { get; set; }
8 | public int? ProductOptionId { get; set; }
9 | public Product? Product { get; set; }
10 | public ProductOption? Option { get; set; }
11 |
12 | public double GetTotalPrice()
13 | {
14 | if (Product is null) return 0;
15 |
16 | var price = Product.DiscountPrice ?? Product.Price;
17 | var additionalCost = Option?.AdditionalCost ?? 0;
18 |
19 | return Quantity * (price + additionalCost);
20 | }
21 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Controllers/CartController.cs:
--------------------------------------------------------------------------------
1 | using Htmx;
2 | using JetSwagStore.Models;
3 | using JetSwagStore.Models.Cart;
4 | using JetSwagStore.Models.Home;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace JetSwagStore.Controllers;
8 |
9 | [Route("[controller]")]
10 | public class CartController : Controller
11 | {
12 | private readonly StoreDbContext db;
13 | private readonly CurrentShoppingCart currentShoppingCart;
14 |
15 | public CartController(StoreDbContext db, CurrentShoppingCart currentShoppingCart)
16 | {
17 | this.db = db;
18 | this.currentShoppingCart = currentShoppingCart;
19 | }
20 |
21 | [HttpPost, Route("")]
22 | public async Task Add([FromForm] UpdateCartRequest input)
23 | {
24 | if (input.Remove)
25 | {
26 | input.Quantity = 0;
27 | }
28 |
29 | var (product, option) =
30 | await db.UpdateShoppingCart(
31 | input.ProductId,
32 | input.ProductOptionId,
33 | currentShoppingCart.Id,
34 | input.Quantity
35 | );
36 |
37 | if (option is not null)
38 | {
39 | return PartialView("_ProductOptions", new ProductWithOptionsViewModel
40 | {
41 | Info = product!,
42 | ProductOptionId = option.Id,
43 | ShouldRenderCartButton = true,
44 | InstantlyShowModal = true,
45 | Swap = true
46 | });
47 | }
48 |
49 | // added via card or view options
50 | return PartialView("_Product", new ProductViewModel
51 | {
52 | Info = product!,
53 | ShouldRenderCartButton = true,
54 | Swap = true
55 | });
56 | }
57 |
58 | [HttpPut, Route("")]
59 | public async Task Update([FromForm] UpdateCartRequest input)
60 | {
61 | if (input.Remove)
62 | {
63 | input.Quantity = 0;
64 | }
65 |
66 | await db.UpdateShoppingCart(
67 | input.ProductId,
68 | input.ProductOptionId,
69 | currentShoppingCart.Id,
70 | input.Quantity
71 | );
72 |
73 | // We should update the UI
74 | var product = await db.Products.FindAsync(input.ProductId);
75 | var model = new ProductViewModel
76 | {
77 | Info = product!,
78 | ShouldRenderCartButton = true,
79 | Swap = true
80 | };
81 |
82 | return PartialView("_CartItems", model);
83 | }
84 |
85 | [HttpGet, Route("")]
86 | public IActionResult Show()
87 | {
88 | return PartialView("_CartItems");
89 | }
90 |
91 | [HttpDelete, Route("")]
92 | public async Task Delete()
93 | {
94 | var cart = await db.ShoppingCarts.FindAsync(currentShoppingCart.Id);
95 | cart?.Items.Clear();
96 | await db.SaveChangesAsync();
97 |
98 | // force page to do a complete refresh
99 | Response.Htmx(htmx => {
100 | htmx.Refresh();
101 | });
102 |
103 | return PartialView("_CartItems");
104 | }
105 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Controllers/HomeController.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Linq;
3 | using Htmx;
4 | using JetSwagStore.Models;
5 | using JetSwagStore.Models.Extensions;
6 | using JetSwagStore.Models.Home;
7 | using Microsoft.AspNetCore.Http.Extensions;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.EntityFrameworkCore;
10 |
11 | namespace JetSwagStore.Controllers;
12 |
13 | public class HomeController : Controller
14 | {
15 | private readonly ILogger logger;
16 | private readonly StoreDbContext db;
17 |
18 | public HomeController(ILogger logger, StoreDbContext db)
19 | {
20 | this.logger = logger;
21 | this.db = db;
22 | }
23 |
24 | [HttpGet, Route("")]
25 | public async Task Index(string? category, string? query)
26 | {
27 | Category? categoryRecord = null;
28 | var hasCategory = false;
29 | if (!string.IsNullOrWhiteSpace(category))
30 | {
31 | categoryRecord = await db.Categories.FirstOrDefaultAsync(c => c.Name == category);
32 | hasCategory = categoryRecord != null;
33 | }
34 |
35 | var hasQuery = !string.IsNullOrWhiteSpace(query);
36 |
37 | var results =
38 | await db.Products
39 | .Include(x => x.Options)
40 | .If(hasCategory, q => q.Where(p => p.Categories.Contains(categoryRecord!)))
41 | .If(hasQuery, q => q.Where(p => EF.Functions.Like(p.Name, $"%{query}%") ))
42 | .Select(p => new ProductViewModel { Info = p })
43 | .ToListAsync();
44 |
45 | var model = new IndexViewModel(results, categoryRecord, query);
46 |
47 | Response.Htmx(h => {
48 | // we want to push the current url
49 | // into the history
50 | h.Push(Request.GetEncodedUrl());
51 | });
52 |
53 | return Request.IsHtmx()
54 | ? PartialView("_Products", model)
55 | : View(model);
56 | }
57 |
58 | [HttpGet, Route("privacy")]
59 | public IActionResult Privacy()
60 | {
61 | return View();
62 | }
63 |
64 | [HttpGet, Route("error")]
65 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
66 | public IActionResult Error()
67 | {
68 | return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
69 | }
70 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Controllers/ProductsController.cs:
--------------------------------------------------------------------------------
1 | using JetSwagStore.Models;
2 | using JetSwagStore.Models.Home;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace JetSwagStore.Controllers;
7 |
8 | [Route("[controller]")]
9 | public class ProductsController : Controller
10 | {
11 | private readonly StoreDbContext db;
12 |
13 | public ProductsController(StoreDbContext db)
14 | {
15 | this.db = db;
16 | }
17 |
18 | [HttpGet, Route("{id}")]
19 | public async Task Show(int id, int? productOptionId, bool? showModal)
20 | {
21 | var product = await db.Products
22 | .Include(p => p.Options)
23 | .Include(p => p.Categories)
24 | .Where(p => p.Id == id)
25 | .Select(p => new ProductWithOptionsViewModel { Info = p, ProductOptionId = productOptionId })
26 | .FirstOrDefaultAsync();
27 |
28 | if (product == null)
29 | return NotFound();
30 |
31 | // set a default selected option if one isn't provided
32 | product.ProductOptionId ??= product.Info.Options.Select(o => o.Id).FirstOrDefault();
33 | product.InstantlyShowModal = showModal.HasValue && showModal.Value;
34 | product.Swap = true;
35 |
36 | return PartialView("_ProductOptions", product);
37 | }
38 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/JetSwagStore.Web.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | JetSwagStore
8 | JetSwagStore
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Cart/CurrentShoppingCart.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models.Cart;
2 |
3 | public class CurrentShoppingCart
4 | {
5 | public int Id { get; set; }
6 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Cart/ShoppingCartExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace JetSwagStore.Models.Cart;
5 |
6 | public static class ShoppingCartExtensions
7 | {
8 | public static IServiceCollection AddShoppingCart(this IServiceCollection services)
9 | {
10 | services.AddScoped();
11 | services.AddScoped();
12 |
13 | return services;
14 | }
15 |
16 | public static IApplicationBuilder UseShoppingCart(this IApplicationBuilder app)
17 | {
18 | return app.UseMiddleware();
19 | }
20 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Cart/ShoppingCartMiddleware.cs:
--------------------------------------------------------------------------------
1 | using JetSwagStore.Models;
2 |
3 | namespace JetSwagStore.Models.Cart;
4 |
5 | public class ShoppingCartMiddleware : IMiddleware
6 | {
7 | private readonly StoreDbContext db;
8 | private readonly CurrentShoppingCart currentShoppingCart;
9 | private const string ShoppingCartCookie = "__shoppingCart";
10 |
11 | public ShoppingCartMiddleware(StoreDbContext db, CurrentShoppingCart currentShoppingCart)
12 | {
13 | this.db = db;
14 | this.currentShoppingCart = currentShoppingCart;
15 | }
16 |
17 | public async Task InvokeAsync(HttpContext context, RequestDelegate next)
18 | {
19 | var id = 0;
20 | if (!context.Request.Cookies.ContainsKey(ShoppingCartCookie))
21 | {
22 | id = await CreateShoppingCart();
23 | }
24 | else
25 | {
26 | if (context.Request.Cookies.TryGetValue(ShoppingCartCookie, out var result) && int.TryParse(result, out id))
27 | {
28 | var cart = await db.ShoppingCarts.FindAsync(id);
29 | id = cart?.Id ?? await CreateShoppingCart();
30 | }
31 | }
32 |
33 | // Will be available through the request
34 | currentShoppingCart.Id = id;
35 | context.Response.Cookies.Append(ShoppingCartCookie, id.ToString());
36 | await next(context);
37 | }
38 |
39 | private async Task CreateShoppingCart()
40 | {
41 | var cart = new ShoppingCart();
42 | await db.ShoppingCarts.AddAsync(cart);
43 | await db.SaveChangesAsync();
44 | return cart.Id;
45 | }
46 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Cart/ShoppingCartViewModelFilter.cs:
--------------------------------------------------------------------------------
1 | using Htmx;
2 | using JetSwagStore.Models.Home;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.AspNetCore.Mvc.Filters;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace JetSwagStore.Models.Cart;
8 |
9 | public class ShoppingCartViewModelFilter : ActionFilterAttribute
10 | {
11 | public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
12 | {
13 | if (context.Controller is Controller controller)
14 | {
15 | var db = context.HttpContext.RequestServices.GetRequiredService();
16 | var currentShoppingCart = context.HttpContext.RequestServices.GetRequiredService();
17 |
18 | var cart = await db.ShoppingCarts.FindAsync(currentShoppingCart.Id);
19 | if (cart != null)
20 | {
21 | var currentCart = new CartViewModel(cart) {
22 | //IsHtmxOutOfBandSwap = context.HttpContext.Request.IsHtmx()
23 | };
24 |
25 | controller.ViewData["CurrentCart"] = currentCart;
26 | }
27 | }
28 |
29 | await next();
30 | }
31 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Cart/UpdateCartRequest.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models.Cart;
2 |
3 | public class UpdateCartRequest
4 | {
5 | public int ProductId { get; set; }
6 | public int? ProductOptionId { get; set; }
7 | public int Quantity { get; set; }
8 | public bool Remove { get; set; }
9 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Components/Categories/CategoriesViewComponent.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace JetSwagStore.Models.Components.Categories;
6 |
7 | public class CategoriesViewComponent : ViewComponent
8 | {
9 | private readonly StoreDbContext _db;
10 |
11 | public CategoriesViewComponent(StoreDbContext db)
12 | {
13 | _db = db;
14 | }
15 |
16 | public async Task InvokeAsync()
17 | {
18 | var model = new CategoriesViewModel
19 | {
20 | Results = await
21 | _db
22 | .Categories
23 | .Select(c => new CategoryViewModel
24 | {
25 | Name = c.Name,
26 | TotalProductCount = c.Products.Count
27 | })
28 | .ToListAsync()
29 | };
30 |
31 | return View("Categories", model);
32 | }
33 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Components/Categories/CategoriesViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models.Components.Categories;
2 |
3 | public class CategoriesViewModel
4 | {
5 | public IEnumerable Results { get; set; }
6 | = Array.Empty();
7 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Components/Categories/CategoryViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models.Components.Categories;
2 |
3 | public class CategoryViewModel
4 | {
5 | public string Name { get; set; } = string.Empty;
6 | public int TotalProductCount { get; set; }
7 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/ErrorViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models;
2 |
3 | public class ErrorViewModel
4 | {
5 | public string? RequestId { get; set; }
6 |
7 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
8 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Home/CartViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 |
3 | namespace JetSwagStore.Models.Home;
4 |
5 | public class CartViewModel
6 | {
7 | public CartViewModel(ShoppingCart cart)
8 | {
9 | Items = cart.Items.Select(i => new CartItemViewModel {
10 | ItemId = i.Id,
11 | Quantity = i.Quantity,
12 | ProductId = i.ProductId.GetValueOrDefault(),
13 | Name = i.Product?.Name ?? string.Empty,
14 | OptionName = i.Option?.Name,
15 | OptionId = i.Option?.Id,
16 | TotalPrice = i.GetTotalPrice(),
17 | ImageUrl = i.Product?.ImageUrl ?? string.Empty
18 | }).ToList();
19 | }
20 |
21 | public int TotalItems => Items.Sum(c => c.Quantity);
22 | public IList Items { get; }
23 | public double TotalPrice => Items.Sum(c => c.TotalPrice);
24 | public CartItemViewModel? HasProduct(int productId) => Items.FirstOrDefault(i => i.ProductId == productId);
25 | public CartItemViewModel? HasProductOption(int productId, int? optionId) => Items.FirstOrDefault(i => i.ProductId == productId && i.OptionId == optionId);
26 | public bool HasItems => Items.Any();
27 | }
28 |
29 | public class CartItemViewModel
30 | {
31 | public int ItemId { get; set; }
32 | public int ProductId { get; set; }
33 | public int Quantity { get; set; }
34 | public string Name { get; set; } = string.Empty;
35 | public string? OptionName { get; set; }
36 | public double TotalPrice { get; set; }
37 | public string ImageUrl { get; set; } = string.Empty;
38 | public int? OptionId { get; set; }
39 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Home/IndexViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace JetSwagStore.Models.Home;
2 |
3 | public class IndexViewModel
4 | {
5 | public IndexViewModel(List results, Category? category, string? query)
6 | {
7 | Category = category;
8 | Query = query;
9 | Results = results;
10 | }
11 |
12 | public string CategoryDisplay => Category is not null ? Category.Name : "All Products";
13 | public bool HasQuery => !string.IsNullOrWhiteSpace(Query);
14 |
15 | public string? Query { get; }
16 | public Category? Category { get; }
17 | public IEnumerable Results { get; }
18 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Home/ProductViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.AspNetCore.Html;
3 |
4 | namespace JetSwagStore.Models.Home;
5 |
6 | public class ProductViewModel
7 | {
8 | public Product Info { get; set; } = null!;
9 |
10 | public bool IsOnSale => Info.DiscountPrice.HasValue;
11 | public bool HasOptions => Info.Options.Any();
12 | public bool ShouldRenderCartButton { get; set; }
13 | public bool Swap { get; set; }
14 |
15 | public HtmlString PriceDisplay
16 | {
17 | get
18 | {
19 | if (HasOptions)
20 | {
21 | var price = IsOnSale ? Info.DiscountPrice : Info.Price;
22 | var prices = Info.Options.Select(p => price + p.AdditionalCost).ToList();
23 | var min = prices.Min();
24 | var max = prices.Max();
25 |
26 | return new HtmlString($"${min:###.00} - ${max:###.00}");
27 | }
28 |
29 | return IsOnSale
30 | ? new HtmlString($"${Info.Price:###.00} ${Info.DiscountPrice:###.00}")
31 | : new HtmlString($"${Info.Price:###.00}");
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/Home/ProductWithOptionsViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Microsoft.AspNetCore.Html;
3 | using Microsoft.AspNetCore.Mvc.Rendering;
4 |
5 | namespace JetSwagStore.Models.Home;
6 |
7 | public class ProductWithOptionsViewModel : ProductViewModel
8 | {
9 | public int ProductId => Info.Id;
10 |
11 | [Display(Name = "Options")]
12 | public int? ProductOptionId { get; set; }
13 | public IEnumerable Options =>
14 | Info.Options.Select(i => new SelectListItem(i.Name, i.Id.ToString())).ToList();
15 | public bool InstantlyShowModal { get; set; }
16 |
17 | public HtmlString CurrentOptionPriceDisplay
18 | {
19 | get
20 | {
21 | if (!ProductOptionId.HasValue)
22 | return base.PriceDisplay;
23 |
24 | // selected option
25 | var item = Info.Options.First(o => o.Id == ProductOptionId);
26 | var total = (Info.DiscountPrice ?? Info.Price) + item.AdditionalCost;
27 |
28 | return new HtmlString($"${total:###.00}");
29 | }
30 | }
31 |
32 |
33 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Models/ShoppingRazorPage.cs:
--------------------------------------------------------------------------------
1 | using JetSwagStore.Models.Home;
2 | using Microsoft.AspNetCore.Mvc.Razor;
3 |
4 | namespace JetSwagStore.Models;
5 |
6 | public abstract class ShoppingRazorPage : RazorPage
7 | {
8 | public CartViewModel Cart =>
9 | ViewBag.CurrentCart as CartViewModel ??
10 | throw new InvalidOperationException("Shopping cart is missing");
11 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Program.cs:
--------------------------------------------------------------------------------
1 | using JetSwagStore.Models;
2 | using JetSwagStore.Models.Cart;
3 | using Microsoft.AspNetCore.Builder;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Hosting;
7 |
8 | var builder = WebApplication.CreateBuilder(args);
9 |
10 | // Add services to the container.
11 | builder.Services
12 | .AddControllersWithViews(options => {
13 | // will get the cart once we've executed an action
14 | options.Filters.Add();
15 | })
16 | .AddRazorRuntimeCompilation();
17 |
18 | builder.Services.AddDbContext();
19 | builder.Services.AddShoppingCart();
20 |
21 | var app = builder.Build();
22 |
23 | // migrate to latest database version
24 | using (var scope = app.Services.CreateScope())
25 | {
26 | var db = scope.ServiceProvider.GetRequiredService();
27 | await db.Database.MigrateAsync();
28 | }
29 |
30 | // Configure the HTTP request pipeline.
31 | if (!app.Environment.IsDevelopment())
32 | {
33 | app.UseExceptionHandler("/Home/Error");
34 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
35 | app.UseHsts();
36 | }
37 |
38 |
39 | app.UseHttpsRedirection();
40 | app.UseStaticFiles();
41 | app.UseShoppingCart();
42 | app.UseRouting();
43 | app.UseAuthorization();
44 | app.MapControllers();
45 |
46 | app.Run();
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "JetSwagStore.Web": {
4 | "commandName": "Project",
5 | "dotnetRunMessages": true,
6 | "launchBrowser": true,
7 | "applicationUrl": "https://localhost:7069;http://localhost:5126",
8 | "environmentVariables": {
9 | "ASPNETCORE_ENVIRONMENT": "Development"
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/Home/Index.cshtml:
--------------------------------------------------------------------------------
1 | @model JetSwagStore.Models.Home.IndexViewModel
2 | @{
3 | ViewData["Title"] = "Home Page";
4 | System.Diagnostics.Debug.Assert(Model != null, nameof(Model) + " != null");
5 | }
6 |
7 | @section Header
8 | {
9 |
10 |
11 |
12 |
JetBrains Swag Store
13 |
14 | Adding More Beauty To Your Brains
15 |
16 |
17 |
18 |
19 | }
20 |
21 |
48 |
49 | @await Html.PartialAsync("_Products")
50 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/Home/Privacy.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Privacy Policy";
3 | }
4 | @ViewData["Title"]
5 |
6 | Use this page to detail your site's privacy policy.
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/Home/_Products.cshtml:
--------------------------------------------------------------------------------
1 | @model JetSwagStore.Models.Home.IndexViewModel
2 | @{
3 | System.Diagnostics.Debug.Assert(Model != null, nameof(Model) + " != null");
4 | }
5 |
6 |
7 |
8 |
9 | @Model.CategoryDisplay
10 | @if (Model.HasQuery)
11 | {
12 | Searched:"@Model.Query"
13 | }
14 |
15 |
16 |
17 | @foreach (var product in Model.Results)
18 | {
19 | @await Html.PartialAsync("_Product", product)
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/Shared/Components/Categories/Categories.cshtml:
--------------------------------------------------------------------------------
1 | @model JetSwagStore.Models.Components.Categories.CategoriesViewModel
2 | @{
3 | System.Diagnostics.Debug.Assert(Model != null, nameof(Model) + " != null");
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/Shared/Error.cshtml:
--------------------------------------------------------------------------------
1 | @model ErrorViewModel
2 | @{
3 | ViewData["Title"] = "Error";
4 | }
5 |
6 | Error.
7 | An error occurred while processing your request.
8 |
9 | @if (Model?.ShowRequestId ?? false)
10 | {
11 |
12 | Request ID: @Model?.RequestId
13 |
14 | }
15 |
16 | Development Mode
17 |
18 | Swapping to Development environment will display more detailed information about the error that occurred.
19 |
20 |
21 | The Development environment shouldn't be enabled for deployed applications.
22 | It can result in displaying sensitive information from exceptions to end users.
23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
24 | and restarting the app.
25 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/Shared/_CartButton.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 | Cart
12 |
13 | @Cart.TotalItems
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | @ViewData["Title"] - JetBrains Swag Store
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
43 |
44 |
45 | @await RenderSectionAsync("Header", false)
46 |
47 |
48 | @RenderBody()
49 | @Html.AntiForgeryToken()
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | @await RenderSectionAsync("Scripts", required: false)
67 |
68 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @inherits ShoppingRazorPage
2 | @using JetSwagStore
3 | @using JetSwagStore.Models
4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
5 | @addTagHelper *, JetSwagStore
6 | @addTagHelper *, Htmx.TagHelpers
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/Views/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "_Layout";
3 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/site.css:
--------------------------------------------------------------------------------
1 | .jetbrains {
2 | background-image: url("../img/header.jpg");
3 | background-size: cover;
4 | position: relative;
5 | }
6 |
7 | .form-control:focus {
8 | border-color: #ff80ff;
9 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px rgba(255, 100, 255, 0.5);
10 | }
11 |
12 | .htmx-indicator{
13 | display:none;
14 | }
15 | .htmx-request .htmx-indicator{
16 | display:inline;
17 | }
18 | .htmx-request.htmx-indicator{
19 | display:inline;
20 | }
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-brands-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-brands-400.eot
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-brands-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-brands-400.woff
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-regular-400.eot
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-regular-400.woff
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/css/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/AppCode.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/AppCode.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/DataGrip.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/DataGrip.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/DotMemory.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/DotMemory.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/DotPeek.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/DotPeek.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/DotTrace.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/DotTrace.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/IntelliJ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/IntelliJ.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/ReSharper.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/ReSharper.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/Rider.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/Rider.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/WebStorm.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/WebStorm.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/header.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/header.jpg
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/img/jetbrains.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khalidabuhakmeh/htmx-aspnetcore/22a7a1a359186c8e0f6417aa2e99d1d4db7f7cb8/JetSwagStore/JetSwagStore.Web/wwwroot/img/jetbrains.png
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/js/extra/client-side-templates.js:
--------------------------------------------------------------------------------
1 | htmx.defineExtension('client-side-templates', {
2 | transformResponse : function(text, xhr, elt) {
3 |
4 | var mustacheTemplate = htmx.closest(elt, "[mustache-template]");
5 | if (mustacheTemplate) {
6 | var data = JSON.parse(text);
7 | var templateId = mustacheTemplate.getAttribute('mustache-template');
8 | var template = htmx.find("#" + templateId);
9 | if (template) {
10 | return Mustache.render(template.innerHTML, data);
11 | } else {
12 | throw "Unknown mustache template: " + templateId;
13 | }
14 | }
15 |
16 | var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
17 | if (handlebarsTemplate) {
18 | var data = JSON.parse(text);
19 | var templateName = handlebarsTemplate.getAttribute('handlebars-template');
20 | return Handlebars.partials[templateName](data);
21 | }
22 |
23 | var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
24 | if (nunjucksTemplate) {
25 | var data = JSON.parse(text);
26 | var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
27 | var template = htmx.find('#' + templateName);
28 | if (template) {
29 | return nunjucks.renderString(template.innerHTML, data);
30 | } else {
31 | return nunjucks.render(templateName, data);
32 | }
33 | }
34 |
35 | return text;
36 | }
37 | });
38 |
--------------------------------------------------------------------------------
/JetSwagStore/JetSwagStore.Web/wwwroot/js/site.js:
--------------------------------------------------------------------------------
1 | document.body.addEventListener('htmx:configRequest', function(evt) {
2 | // not needed for GET requests
3 | if (evt.detail.verb === 'GET') {
4 | return;
5 | }
6 |
7 | let antiForgeryRequestToken = document.querySelector('input[name="__RequestVerificationToken"]')[0];
8 | if (antiForgeryRequestToken) {
9 | evt.detail.parameters['__RequestVerificationToken'] = antiForgeryRequestToken.value;
10 | console.log(antiForgeryRequestToken);
11 | }
12 | });
13 |
14 | function showModal() {
15 | const backdrop = document.getElementById("modal-backdrop");
16 | const modal = document.getElementById("modal");
17 |
18 | setTimeout(() => {
19 | modal.classList.add("show")
20 | backdrop.classList.add("show")
21 | }, 10);
22 | }
23 |
24 | function closeModal() {
25 | const container = document.getElementById("product-modal-container");
26 | const backdrop = document.getElementById("modal-backdrop");
27 | const modal = document.getElementById("modal");
28 |
29 | modal.classList.remove("show")
30 | backdrop.classList.remove("show")
31 |
32 | setTimeout(function() {
33 | container.removeChild(backdrop)
34 | container.removeChild(modal)
35 | }, 200)
36 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Khalid Abuhakmeh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "6.0",
4 | "rollForward": "latestMajor",
5 | "allowPrerelease": true
6 | }
7 | }
--------------------------------------------------------------------------------
/htmx-aspnetcore.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30114.105
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Exercises", "Exercises", "{91212099-7C38-4BF6-8DB4-69A6E32BBE81}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JetSwagStore", "JetSwagStore", "{23957457-27D1-43B8-B311-0B8746A02922}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exercises.End", "Exercises\Exercises.End\Exercises.End.csproj", "{9F221DED-210C-4907-BE8A-7D0D489F9F70}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JetSwagStore.Models", "JetSwagStore\JetSwagStore.Models\JetSwagStore.Models.csproj", "{244DD436-55EC-40C5-BB86-0548616213AF}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JetSwagStore.Web", "JetSwagStore\JetSwagStore.Web\JetSwagStore.Web.csproj", "{6D8B6CF0-815B-43F8-8202-C75A4608B17D}"
15 | EndProject
16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exercises.Start", "Exercises\Exercises.Start\Exercises.Start.csproj", "{17DCDDA7-058F-4FD8-BE87-45BD77B255F2}"
17 | EndProject
18 | Global
19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
20 | Debug|Any CPU = Debug|Any CPU
21 | Release|Any CPU = Release|Any CPU
22 | EndGlobalSection
23 | GlobalSection(SolutionProperties) = preSolution
24 | HideSolutionNode = FALSE
25 | EndGlobalSection
26 | GlobalSection(NestedProjects) = preSolution
27 | {9F221DED-210C-4907-BE8A-7D0D489F9F70} = {91212099-7C38-4BF6-8DB4-69A6E32BBE81}
28 | {244DD436-55EC-40C5-BB86-0548616213AF} = {23957457-27D1-43B8-B311-0B8746A02922}
29 | {6D8B6CF0-815B-43F8-8202-C75A4608B17D} = {23957457-27D1-43B8-B311-0B8746A02922}
30 | {17DCDDA7-058F-4FD8-BE87-45BD77B255F2} = {91212099-7C38-4BF6-8DB4-69A6E32BBE81}
31 | EndGlobalSection
32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
33 | {9F221DED-210C-4907-BE8A-7D0D489F9F70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {9F221DED-210C-4907-BE8A-7D0D489F9F70}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {9F221DED-210C-4907-BE8A-7D0D489F9F70}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {9F221DED-210C-4907-BE8A-7D0D489F9F70}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {244DD436-55EC-40C5-BB86-0548616213AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {244DD436-55EC-40C5-BB86-0548616213AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {244DD436-55EC-40C5-BB86-0548616213AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {244DD436-55EC-40C5-BB86-0548616213AF}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {6D8B6CF0-815B-43F8-8202-C75A4608B17D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42 | {6D8B6CF0-815B-43F8-8202-C75A4608B17D}.Debug|Any CPU.Build.0 = Debug|Any CPU
43 | {6D8B6CF0-815B-43F8-8202-C75A4608B17D}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {6D8B6CF0-815B-43F8-8202-C75A4608B17D}.Release|Any CPU.Build.0 = Release|Any CPU
45 | {17DCDDA7-058F-4FD8-BE87-45BD77B255F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46 | {17DCDDA7-058F-4FD8-BE87-45BD77B255F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
47 | {17DCDDA7-058F-4FD8-BE87-45BD77B255F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
48 | {17DCDDA7-058F-4FD8-BE87-45BD77B255F2}.Release|Any CPU.Build.0 = Release|Any CPU
49 | EndGlobalSection
50 | EndGlobal
51 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # HTMX For ASP.NET Core Developers Video Guide
2 |
3 | This repository holds the sample projects used for my HTMX for ASP.NET Core developer's guide.
4 |
5 | ## Getting Started
6 |
7 | To get started, you'll need a minimum of .NET 6 SDK installed on your development machine. You can get the
8 | latest SDK from [dot.net](https://dotnet.microsoft.com/download). While this course uses the latest
9 | version of .NET, the patterns and approaches can be adapted to target ASP.NET Core applications as low as .NET 3.1.
10 |
11 | This guide uses [JetBrains Rider](https://jetbrains.com/rider) in its videos, and it is highly recommended, but is not required.
12 |
13 | ## Repository Structure
14 |
15 | There are two logical solutions in this repository: Exercises and JetSwag Store.
16 | And each project exists in two versions: Start and End. For ease of navigating and following along,
17 | they are included in a single .NET solution.
18 |
19 | You can follow along by beginning with the `Start` version of a project. For folks interested in
20 | seeing the final result, they can look at the `End` version of a project.
21 |
22 | ```console
23 | |- Exercises (solution folder)
24 | |- Exercises.Start
25 | |- Exercises.End
26 | |- JetSwagStore (solution folder)
27 | |- JetSwagStore.Start
28 | |- JetSwagStore.End
29 | |- JetSwagStore.Shared (EF Core SQLite backend)
30 | ```
31 |
32 | ### Samples
33 |
34 | Samples will walk you through common patterns you may use when adopting HTMX. This is similar to Kata work.
35 |
36 | Your goal is to make every box return an expected result. From the start, no functionality
37 | will be connected.
38 |
39 | ### JetSwag Store
40 |
41 | This is a real-world sample, taking a working ASP.NET Core application and enhancing it with HTMX.
42 |
43 | ## Videos
44 |
45 | [Link to Videos](https://www.jetbrains.com/guide/dotnet/tutorials/htmx-aspnetcore/)
46 |
47 | ## License
48 |
49 | MIT License
50 |
--------------------------------------------------------------------------------