Merge pull request #10 from bdfin/add-about-page

Add about page
This commit is contained in:
2024-03-14 12:26:59 +00:00
committed by GitHub
14 changed files with 146 additions and 89 deletions

View File

@@ -3,7 +3,7 @@
<div class="flex flex-col min-h-screen fade-in">
<NavBar/>
<div class="flex-1 px-4 md:px-12 py-4">
<div class="flex-1 px-4 md:px-12 lg:px-24 xl:px-32 py-4">
@Body
</div>

View File

@@ -4,14 +4,7 @@
<PageTitle>About - Beau Findlay</PageTitle>
@if (comingSoon)
{
<div class="text-center">
<h1 class="text-4xl">Coming soon...</h1>
</div>
}
else
{
<div class="text-center pb-4" id="@TopSection">
@if (!hasPreviouslyRendered)
{
<h1 class="text-4xl">
@@ -20,33 +13,62 @@ else
}
else
{
<h1 class="text-4xl">This app<span class="blinking-cursor">|</span></h1>
<h1 class="text-4xl">This app</h1>
}
</div>
<p class="py-4 text-xl">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 <Anchor Href="https://github.com/bdfin/my-portfolio">GitHub</Anchor>.</p>
<nav class="flex items-center justify-center py-8 space-x-8">
<a @onclick="() => ScrollToElementAsync(FrontEndSection)" class="underline underline-offset-2 cursor-pointer">Front-end</a>
<a @onclick="() => ScrollToElementAsync(BackEndSection)" class="underline underline-offset-2 cursor-pointer">Back-end</a>
<a @onclick="() => ScrollToElementAsync(HostingSection)" class="underline underline-offset-2 cursor-pointer">Hosting</a>
</nav>
<section class="py-6" id="@FrontEndSection">
<h2 class="text-2xl">Front-end</h2>
<p class="py-4 text-xl">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 <Anchor Href="https://github.com/bdfin/my-portfolio">GitHub</Anchor>.</p>
<p class="py-4">I wanted to create a decent, modern client-side experience for this app and given my <em class="text-xs">(very...)</em> limited front-end expertise I decided to choose <Anchor Href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">.NET Blazor Webassembly</Anchor>. 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.</p>
<p class="pt-4 pb-8 text-xl">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!</p>
<p class="py-4">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. <Anchor Href="https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-8.0#blazor-server">Blazor Server</Anchor> initially generates content on the server and utilises web-sockets to communicate dynamic UI updates with the client without requiring a page load, whereas <Anchor Href="https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-8.0#blazor-webassembly">Blazor Webassembly</Anchor> 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.</p>
<section class="py-12 text-lg" id="@FrontEndSection">
<h2 class="text-3xl pb-4">Front-end: <img src="images/blazor-logo.png" class="inline w-auto h-6" alt="blazor logo"/> .NET Blazor WASM</h2>
<p class="py-4">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 <Anchor Href="https://azure.microsoft.com/en-gb/products/app-service/static">Azure Static Web App</Anchor>. You can read more about this in the <a @onclick="() => ScrollToElementAsync(HostingSection)" class="underline underline-offset-2 cursor-pointer">hosting</a> section.</p>
</section>
<p class="py-4">I wanted to create a decent, modern client-side experience for this app and given my <em class="text-xs">(very...)</em> limited front-end expertise I decided to choose <Anchor Href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">.NET Blazor Webassembly</Anchor>. 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.</p>
<section class="py-6" id="@BackEndSection">
<h2 class="text-2xl pb-4">Back-end</h2>
<p class="my-4">As the </p>
</section>
<p class="py-4">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). <Anchor Href="https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-8.0#blazor-server">Blazor Server</Anchor> initially generates content on the server and utilises web-sockets to communicate dynamic UI updates with the client without requiring a page load, whereas <Anchor Href="https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-8.0#blazor-webassembly">Blazor Webassembly</Anchor> 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.</p>
<p class="py-4">As Blazor server requires a dedicated server to host the application, I chose the webassembly model to enable free hosting using an <Anchor Href="https://azure.microsoft.com/en-gb/products/app-service/static">Azure Static Web App</Anchor>. You can read more about this in the <a @onclick="() => ScrollToElementAsync(HostingSection)" class="underline underline-offset-2 cursor-pointer">hosting</a> section.</p>
<p class="py-4">This app is styled using a cool CSS framework called <Anchor Href="https://tailwindcss.com/">TailwindCSS</Anchor>. <Anchor Href="https://postcss.org/">PostCSS</Anchor> 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.</p>
</section>
<section class="py-12 text-lg" id="@BackEndSection">
<h2 class="text-3xl pb-4">Back-end: <img src="images/azure-function-logo.png" class="inline w-auto h-6" alt="azure function app logo"/> .NET Azure Functions API</h2>
<p class="py-4">There is a very minimal API used as the back-end of this app to allow users to contact me directly via the <NavLink href="/contact" class="underline underline-offset-2">contact</NavLink> page. This will be expanded to serve the technical blog I'm building as a new feature that will be available soon.</p>
<p class="pt-4 pb-2">The contact API endpoint currently:</p>
<ul class="list-disc pl-8 pb-4">
<li>Validates a <Anchor Href="https://www.google.com/recaptcha/about/">Google Recaptcha</Anchor> token to protect against fraudulent submissions.</li>
<li>Builds a HTML email from the information provided in the form.</li>
<li>Sends an email directly to my inbox using the <Anchor Href="https://sendgrid.com/en-us">SendGrid</Anchor> API.</li>
</ul>
<p class="py-4">The API is written in .NET 8 using <Anchor Href="https://azure.microsoft.com/en-gb/products/functions">Azure Serverless Functions</Anchor> with HTTP triggers to act as API endpoints. For larger scale projects I would almost always opt for a fully-featured <Anchor Href="https://dotnet.microsoft.com/en-us/apps/aspnet/apis">Web API</Anchor>, however Azure Functions provide automatic elastic scaling with consumption-based billing and a generous free-tier, making them perfect for smaller projects like this.</p>
</section>
<section class="py-6" id="@HostingSection">
<h2 class="text-2xl pb-4">Hosting</h2>
<p></p>
</section>
}
<section class="py-12 text-lg" id="@HostingSection">
<h2 class="text-3xl pb-4">Hosting: <img src="images/azure-static-web-app-logo.png" class="inline w-auto h-6" alt="azure static web app logo"/> Microsoft Azure Static Web App</h2>
<p class="py-4">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 <Anchor Href="https://azure.microsoft.com/en-gb/products/app-service/static">Static Web App</Anchor> 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.</p>
<p class="py-4">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.</p>
<p class="py-4">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 <Anchor Href="https://azure.microsoft.com/en-gb/products/azure-sql/database">Azure SQL database</Anchor> where I'll document the full process of writing and deploying this app so check back again soon.</p>
</section>
<nav class="flex items-center justify-center pb-8">
<a @onclick="() => ScrollToElementAsync(TopSection)" class="underline underline-offset-2 cursor-pointer">Top</a>
</nav>
<AnchorNavigation/>
@@ -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)
{

View File

@@ -1,7 +1,5 @@
@page "/"
@implements IDisposable
@inject IJSRuntime JSRuntime
<PageTitle>Home - Beau Findlay</PageTitle>
@@ -12,19 +10,19 @@
<Typewriter Text="Hi, I'm Beau."/>
</h1>
<p class="text-xl mt-4">
<p class="text-xl py-4">
<Typewriter Name="@TypewriterConstants.Name.IntroComplete" Text="I'm a UK-based software engineer and I love building cool stuff."/>
</p>
<h2 class="text-2xl mt-16 font-semibold">
<h2 class="text-2xl pt-16 font-semibold">
<Typewriter Text="A bit about me"/>
</h2>
<p class="text-xl mt-4">
<p class="text-xl py-4">
<Typewriter Text="I mostly specialise in back-end C#/.NET development and I've built systems that scale for hundreds-of-thousands of global users."/>
</p>
<p class="text-xl mt-4">
<p class="text-xl py-4">
<Typewriter Text="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."/>
</p>
}
@@ -32,13 +30,13 @@ else
{
<h1 class="text-4xl">Hi, I'm Beau.</h1>
<p class="text-xl mt-4">I'm a UK-based software engineer and I love building cool stuff.</p>
<p class="text-xl py-4">I'm a UK-based software engineer and I love building cool stuff.</p>
<h2 class="text-3xl mt-16 font-semibold">A bit about me</h2>
<h2 class="text-3xl pt-16 font-semibold">A bit about me</h2>
<p class="text-xl mt-4">I mostly specialise in back-end C#/.NET development and I've built systems that scale for hundreds-of-thousands of global users.</p>
<p class="text-xl py-4">I mostly specialise in back-end C#/.NET development and I've built systems that scale for hundreds-of-thousands of global users.</p>
<p class="text-xl mt-4">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 <Anchor Href="https://unhurd.co.uk">un:hurd</Anchor>.</p>
<p class="text-xl py-4">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 <Anchor Href="https://unhurd.co.uk">un:hurd</Anchor>.</p>
}
@@ -50,8 +48,6 @@ else
{
if (firstRender)
{
Typewriter.OnAllTypingCompleted += HandleTypingCompleted;
var renderedBeforeAsString = await JSRuntime.InvokeAsync<string>("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;
}
}

View File

@@ -2,7 +2,11 @@
module.exports = {
content: ["./**/*.{razor,html,cshtml}"],
theme: {
extend: {},
extend: {
fontFamily: {
cascadia: ["Cascadia Code", "mono-space"]
}
},
},
plugins: [],
}

View File

@@ -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 }

View File

@@ -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;
}
.dark\:text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
@media (min-width: 1024px) {
.lg\:px-24 {
padding-left: 6rem;
padding-right: 6rem;
}
}
@media (min-width: 1280px) {
.xl\:px-32 {
padding-left: 8rem;
padding-right: 8rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -5,6 +5,14 @@
<script id="cookieyes"
type="text/javascript"
src="https://cdn-cookieyes.com/client_data/a05e8ecc917e725a2226b46a/script.js"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-958BPT37HR"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-958BPT37HR');
</script>
<meta charset="utf-8"/>
<meta name="viewport"
content="width=device-width, initial-scale=1.0"/>
@@ -17,8 +25,7 @@
<title>Beau Findlay</title>
<base href="/"/>
<link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="images/favicon-16x16.png">
<link rel="icon" type="image/png" href="images/logo.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet"
href="css/app.min.css"/>
@@ -29,7 +36,7 @@
referrerpolicy="no-referrer"/>
</head>
<body class="bg-black font-mono text-slate-50 min-h-screen antialiased">
<body class="bg-black font-cascadia text-slate-50 min-h-screen antialiased">
<div id="app"
class="h-full">
<div class="flex items-center justify-center text-2xl">