diff --git a/BeauFindlay/src/BeauFindlay.Client/Layout/MainLayout.razor b/BeauFindlay/src/BeauFindlay.Client/Layout/MainLayout.razor index d7ef8e5..5617507 100644 --- a/BeauFindlay/src/BeauFindlay.Client/Layout/MainLayout.razor +++ b/BeauFindlay/src/BeauFindlay.Client/Layout/MainLayout.razor @@ -3,7 +3,7 @@
-
+
@Body
diff --git a/BeauFindlay/src/BeauFindlay.Client/Pages/About.razor b/BeauFindlay/src/BeauFindlay.Client/Pages/About.razor index c4a4bf8..279c2d1 100644 --- a/BeauFindlay/src/BeauFindlay.Client/Pages/About.razor +++ b/BeauFindlay/src/BeauFindlay.Client/Pages/About.razor @@ -4,14 +4,7 @@ About - Beau Findlay -@if (comingSoon) -{ -
-

Coming soon...

-
-} -else -{ +
@if (!hasPreviouslyRendered) {

@@ -20,33 +13,62 @@ else } else { -

This app|

+

This app

} +
-

Below is a brief outline of how this app is made and what technologies are used. If you'd like to dive straight in then full project is available on my GitHub.

+ -
-

Front-end

+

Below is an overview of how this simple app is made and what technologies are used. If you'd like to dive straight in, the full project is available on my GitHub.

-

I wanted to create a decent, modern client-side experience for this app and given my (very...) limited front-end expertise I decided to choose .NET Blazor Webassembly. Blazor is Microsoft's take on component-based SPAs (single page applications) and offers us back-end focussed devs a way of producing decent client experiences without needing to dive into another front-end specific technology.

+

I'm planning to integrate a simple blog as part of this app that will dive into more specific implementation details so check back soon for more!

-

Blazor traditionally came in two flavours, server and webassembly with an additional third option (Blazor Web App) recently released with .NET 8 which is an amalgamation of both. Blazor Server initially generates content on the server and utilises web-sockets to communicate dynamic UI updates with the client without requiring a page load, whereas Blazor Webassembly downloads the entire app to the client browser on first load alongside a light-weight .NET run-time to execute code directly on the browsers UI thread.

+
+

Front-end: blazor logo .NET Blazor WASM

-

As Blazor server requires a dedicated server to host the application, I chose Blazor webassembly to enable me to host this app for free using an Azure Static Web App. You can read more about this in the hosting section.

-
+

I wanted to create a decent, modern client-side experience for this app and given my (very...) limited front-end expertise I decided to choose .NET Blazor Webassembly. Blazor is Microsoft's take on component-based SPAs (single page applications) and offers us back-end focussed devs a way of producing decent client experiences without needing to dive into another front-end specific technology.

-
-

Back-end

-

As the

-
+

Blazor traditionally came in two flavours, server and webassembly with an additional third option (Blazor Web App) recently released with .NET 8 which can offer the functionality of both, alongside traditional SSR (server-side rendering). Blazor Server initially generates content on the server and utilises web-sockets to communicate dynamic UI updates with the client without requiring a page load, whereas Blazor Webassembly downloads the entire app to the client browser on first load alongside a light-weight .NET run-time to execute code directly on the browsers UI thread.

+ +

As Blazor server requires a dedicated server to host the application, I chose the webassembly model to enable free hosting using an Azure Static Web App. You can read more about this in the hosting section.

+ +

This app is styled using a cool CSS framework called TailwindCSS. PostCSS is used alongside Tailwind to generate a lightweight stylesheet based only on the parts of the framework that are used, as oppose to including a everything the framework offers.

+
+ +
+

Back-end: azure function app logo .NET Azure Functions API

+ +

There is a very minimal API used as the back-end of this app to allow users to contact me directly via the contact page. This will be expanded to serve the technical blog I'm building as a new feature that will be available soon.

+ +

The contact API endpoint currently:

+
    +
  • Validates a Google Recaptcha token to protect against fraudulent submissions.
  • +
  • Builds a HTML email from the information provided in the form.
  • +
  • Sends an email directly to my inbox using the SendGrid API.
  • +
+ +

The API is written in .NET 8 using Azure Serverless Functions with HTTP triggers to act as API endpoints. For larger scale projects I would almost always opt for a fully-featured Web API, however Azure Functions provide automatic elastic scaling with consumption-based billing and a generous free-tier, making them perfect for smaller projects like this.

+ +
-
-

Hosting

-

-
-} +
+

Hosting: azure static web app logo Microsoft Azure Static Web App

+ +

The goal of this project was to learn some new technologies and host the app as cheaply as possible. With this in mind I decided to go with a Static Web App hosted on Microsoft Azure. Static Web Apps offer global distribution of static assets (the Blazor Webassembly app in this case) and offer integrated hosting for Azure Function App APIs.

+ +

Another cool feature of Static Web Apps is Azure's integration with GitHub actions to deploy both the client and server simultaneously and provide automatically deployed staging environments for pull-requests opened to the main branch. This made testing deployed changes much easier and cheaper than deploying an isolated testing/GA environment before releasing to the live version of the app.

+ +

Using Static Web Apps on Azure has meant that I have been able to build, deploy and serve this site and API completely free (with the exception of the domain name). The next thing on the roadmap is building a simple blog using an Azure SQL database where I'll document the full process of writing and deploying this app so check back again soon.

+
+ @@ -55,9 +77,9 @@ else private const string FrontEndSection = "front-end"; private const string BackEndSection = "back-end"; private const string HostingSection = "hosting"; + private const string TopSection = "top"; private bool hasPreviouslyRendered; - private bool comingSoon = true; protected override async Task OnAfterRenderAsync(bool firstRender) { diff --git a/BeauFindlay/src/BeauFindlay.Client/Pages/Home.razor b/BeauFindlay/src/BeauFindlay.Client/Pages/Home.razor index 85371db..83111a0 100644 --- a/BeauFindlay/src/BeauFindlay.Client/Pages/Home.razor +++ b/BeauFindlay/src/BeauFindlay.Client/Pages/Home.razor @@ -1,7 +1,5 @@ @page "/" -@implements IDisposable - @inject IJSRuntime JSRuntime Home - Beau Findlay @@ -12,19 +10,19 @@ -

+

-

+

-

+

-

+

} @@ -32,13 +30,13 @@ else {

Hi, I'm Beau.

-

I'm a UK-based software engineer and I love building cool stuff.

+

I'm a UK-based software engineer and I love building cool stuff.

-

A bit about me

+

A bit about me

-

I mostly specialise in back-end C#/.NET development and I've built systems that scale for hundreds-of-thousands of global users.

+

I mostly specialise in back-end C#/.NET development and I've built systems that scale for hundreds-of-thousands of global users.

-

I've worked with businesses at all sizes and stages and I'm currently heading up the tech as CTO at a cool startup called un:hurd.

+

I've worked with businesses at all sizes and stages and I'm currently heading up the tech as CTO at a cool startup called un:hurd.

} @@ -50,8 +48,6 @@ else { if (firstRender) { - Typewriter.OnAllTypingCompleted += HandleTypingCompleted; - var renderedBeforeAsString = await JSRuntime.InvokeAsync("localStorage.getItem", ComponentKey); var previousValue = hasPreviouslyRendered; @@ -69,14 +65,4 @@ else } } - private static void HandleTypingCompleted() - { - Console.WriteLine("Typewriter finished typing."); - } - - public void Dispose() - { - Typewriter.OnAllTypingCompleted -= HandleTypingCompleted; - } - } \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Client/tailwind.config.js b/BeauFindlay/src/BeauFindlay.Client/tailwind.config.js index ffd96f1..bc2c35b 100644 --- a/BeauFindlay/src/BeauFindlay.Client/tailwind.config.js +++ b/BeauFindlay/src/BeauFindlay.Client/tailwind.config.js @@ -2,7 +2,11 @@ module.exports = { content: ["./**/*.{razor,html,cshtml}"], theme: { - extend: {}, + extend: { + fontFamily: { + cascadia: ["Cascadia Code", "mono-space"] + } + }, }, plugins: [], } \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/css/app.css b/BeauFindlay/src/BeauFindlay.Client/wwwroot/css/app.css index e93f0ec..c7afe85 100644 --- a/BeauFindlay/src/BeauFindlay.Client/wwwroot/css/app.css +++ b/BeauFindlay/src/BeauFindlay.Client/wwwroot/css/app.css @@ -2,6 +2,11 @@ @tailwind components; @tailwind utilities; +@font-face { + font-family: "Cascadia Code"; + src: url("../fonts/CascadiaCode.woff2"); +} + @keyframes blink { from, to { opacity: 1 } 50% { opacity: 0 } diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/css/app.min.css b/BeauFindlay/src/BeauFindlay.Client/wwwroot/css/app.min.css index 399f64d..3e3e2ee 100644 --- a/BeauFindlay/src/BeauFindlay.Client/wwwroot/css/app.min.css +++ b/BeauFindlay/src/BeauFindlay.Client/wwwroot/css/app.min.css @@ -1,5 +1,5 @@ /* -! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com +! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com */ /* @@ -32,11 +32,9 @@ 4. Use the user's configured `sans` font-family by default. 5. Use the user's configured `sans` font-feature-settings by default. 6. Use the user's configured `sans` font-variation-settings by default. -7. Disable tap highlights on iOS */ -html, -:host { +html { line-height: 1.5; /* 1 */ -webkit-text-size-adjust: 100%; @@ -46,14 +44,12 @@ html, -o-tab-size: 4; tab-size: 4; /* 3 */ - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ font-feature-settings: normal; /* 5 */ font-variation-settings: normal; /* 6 */ - -webkit-tap-highlight-color: transparent; - /* 7 */ } /* @@ -125,10 +121,8 @@ strong { } /* -1. Use the user's configured `mono` font-family by default. -2. Use the user's configured `mono` font-feature-settings by default. -3. Use the user's configured `mono` font-variation-settings by default. -4. Correct the odd `em` font sizing in all browsers. +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. */ code, @@ -137,12 +131,8 @@ samp, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ - font-feature-settings: normal; - /* 2 */ - font-variation-settings: normal; - /* 3 */ font-size: 1em; - /* 4 */ + /* 2 */ } /* @@ -569,11 +559,6 @@ video { margin-right: auto; } -.my-4 { - margin-top: 1rem; - margin-bottom: 1rem; -} - .ml-3 { margin-left: 0.75rem; } @@ -638,6 +623,10 @@ video { height: 100%; } +.h-7 { + height: 1.75rem; +} + .min-h-screen { min-height: 100vh; } @@ -658,6 +647,10 @@ video { width: 2rem; } +.w-auto { + width: auto; +} + .w-full { width: 100%; } @@ -688,6 +681,10 @@ video { cursor: pointer; } +.list-disc { + list-style-type: disc; +} + .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -799,30 +796,50 @@ video { padding-bottom: 1rem; } +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + .py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; } -.py-8 { - padding-top: 2rem; - padding-bottom: 2rem; +.pb-2 { + padding-bottom: 0.5rem; } .pb-4 { padding-bottom: 1rem; } +.pb-8 { + padding-bottom: 2rem; +} + +.pl-8 { + padding-left: 2rem; +} + +.pt-4 { + padding-top: 1rem; +} + .pt-8 { padding-top: 2rem; } +.pt-16 { + padding-top: 4rem; +} + .text-center { text-align: center; } -.font-mono { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +.font-cascadia { + font-family: Cascadia Code, mono-space; } .text-2xl { @@ -935,10 +952,6 @@ video { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.outline { - outline-style: solid; -} - .ring-1 { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); @@ -954,6 +967,12 @@ video { --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); } +@font-face { + font-family: "Cascadia Code"; + + src: url("../fonts/CascadiaCode.woff2"); +} + @keyframes blink { from, to { opacity: 1 @@ -1061,6 +1080,17 @@ body::-webkit-scrollbar-thumb { background-color: rgb(31 41 55 / var(--tw-bg-opacity)); } +@media (prefers-color-scheme: dark) { + .dark\:fill-gray-300 { + fill: #d1d5db; + } + + .dark\:text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); + } +} + @media (min-width: 640px) { .sm\:col-span-2 { grid-column: span 2 / span 2; @@ -1102,13 +1132,16 @@ body::-webkit-scrollbar-thumb { } } -@media (prefers-color-scheme: dark) { - .dark\:fill-gray-300 { - fill: #d1d5db; +@media (min-width: 1024px) { + .lg\:px-24 { + padding-left: 6rem; + padding-right: 6rem; } +} - .dark\:text-gray-600 { - --tw-text-opacity: 1; - color: rgb(75 85 99 / var(--tw-text-opacity)); +@media (min-width: 1280px) { + .xl\:px-32 { + padding-left: 8rem; + padding-right: 8rem; } } \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/fonts/CascadiaCode.woff2 b/BeauFindlay/src/BeauFindlay.Client/wwwroot/fonts/CascadiaCode.woff2 new file mode 100644 index 0000000..8865499 Binary files /dev/null and b/BeauFindlay/src/BeauFindlay.Client/wwwroot/fonts/CascadiaCode.woff2 differ diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/azure-function-logo.png b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/azure-function-logo.png new file mode 100644 index 0000000..6232b31 Binary files /dev/null and b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/azure-function-logo.png differ diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/azure-static-web-app-logo.png b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/azure-static-web-app-logo.png new file mode 100644 index 0000000..0b96333 Binary files /dev/null and b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/azure-static-web-app-logo.png differ diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/blazor-logo.png b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/blazor-logo.png new file mode 100644 index 0000000..fb308a8 Binary files /dev/null and b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/blazor-logo.png differ diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/favicon-16x16.png b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/favicon-16x16.png deleted file mode 100644 index f78dd85..0000000 Binary files a/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/favicon-16x16.png and /dev/null differ diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/favicon-32x32.png b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/favicon-32x32.png deleted file mode 100644 index 64fcf31..0000000 Binary files a/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/favicon-32x32.png and /dev/null differ diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/logo.png b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/logo.png new file mode 100644 index 0000000..1985d32 Binary files /dev/null and b/BeauFindlay/src/BeauFindlay.Client/wwwroot/images/logo.png differ diff --git a/BeauFindlay/src/BeauFindlay.Client/wwwroot/index.html b/BeauFindlay/src/BeauFindlay.Client/wwwroot/index.html index 874083d..1aa61bf 100644 --- a/BeauFindlay/src/BeauFindlay.Client/wwwroot/index.html +++ b/BeauFindlay/src/BeauFindlay.Client/wwwroot/index.html @@ -5,6 +5,14 @@ + + @@ -17,8 +25,7 @@ Beau Findlay - - + @@ -29,7 +36,7 @@ referrerpolicy="no-referrer"/> - +