Add about page

This commit is contained in:
Beau Findlay
2024-04-26 15:57:15 +01:00
parent 474d0ed5bf
commit fcfff25ec7
9 changed files with 229 additions and 60 deletions

View File

@@ -1,7 +1,158 @@
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { Fragment } from "react"; import { Fragment, ReactNode } from "react";
import Subtitle from "./Subtitle"; import { SiAzurefunctions, SiMicrosoftazure, SiReact } from "react-icons/si";
import { Link } from "react-router-dom";
import buildClassNames from "../helpers/cssClassFormatter"; import buildClassNames from "../helpers/cssClassFormatter";
import AnchorLink from "./AnchorLink";
import List from "./List";
import ListItem from "./ListItem";
import Subtitle from "./Subtitle";
import Text from "./Text";
interface AboutTab {
tabName: string;
title: ReactNode;
subtitle: string;
content: ReactNode[];
}
const tabs: AboutTab[] = [
{
tabName: "Front-end",
title: (
<Subtitle>
Front-end <SiReact className="ml-2" />
</Subtitle>
),
subtitle: "React + TypeScript",
content: [
<Text>
This app was originally made using{" "}
<AnchorLink href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">
.NET Blazor WASM
</AnchorLink>{" "}
however I recently decided to start learning{" "}
<AnchorLink href="https://react.dev/">React</AnchorLink> and saw this as
a good oppurtunity to solidify some knowledge by re-writing this app
from the ground up using React with{" "}
<AnchorLink href="https://www.typescriptlang.org/">
TypeScript
</AnchorLink>
.
</Text>,
<Text>
Overall I've found building front-end apps far more enjoyable using
React & TypeScript dispite the steep learning-curve coming from a purely
.NET & C# background. Both the developer experience and performance of
the app have improved dramatically after switching front-end
technologies.
</Text>,
<Text>
This app is styled using a cool CSS framework called{" "}
<AnchorLink href="https://tailwindcss.com/">TailwindCSS</AnchorLink>.{" "}
<AnchorLink href="https://postcss.org/">PostCSS</AnchorLink> 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.
</Text>,
],
},
{
tabName: "Back-end",
title: (
<Subtitle>
Back-end <SiAzurefunctions className="ml-2" />
</Subtitle>
),
subtitle: ".NET Azure Functions API",
content: [
<Text>
There is a very minimal API used as the back-end of this app to allow
users to contact me directly via the{" "}
<Link
to="/contact"
className="underline underline-offset-2 hover:underline-offset-4"
>
contact
</Link>{" "}
page. This will be expanded to serve the technical blog I'm building as
a new feature that will be available soon.
</Text>,
<Text>The contact API endpoint currently:</Text>,
<List className="pb-4 pt-2">
<ListItem>
Validates a{" "}
<AnchorLink href="https://www.google.com/recaptcha/about/">
Google reCAPTCHA
</AnchorLink>{" "}
token to protect against fraudulent submissions.
</ListItem>
<ListItem>
Builds a HTML email from the information provided in the form.
</ListItem>
<ListItem>
Sends an email directly to my inbox using the{" "}
<AnchorLink href="https://sendgrid.com/en-us">SendGrid</AnchorLink>{" "}
API.
</ListItem>
</List>,
<Text>
The API is written in .NET 8 using{" "}
<AnchorLink href="https://azure.microsoft.com/en-gb/products/functions">
Azure Serverless Functions
</AnchorLink>{" "}
with a HTTP trigger to act as an API endpoint. For larger scale projects
I would almost always opt for a fully-featured{" "}
<AnchorLink href="https://dotnet.microsoft.com/en-us/apps/aspnet/apis">
Web API
</AnchorLink>
, however Azure Functions provide automatic elastic scaling with
consumption-based billing and a generous free-tier, making them perfect
for smaller projects like this.
</Text>,
],
},
{
tabName: "Hosting",
title: (
<Subtitle>
Hosting <SiMicrosoftazure className="ml-2" />
</Subtitle>
),
subtitle: "Microsoft Azure Static Web App",
content: [
<Text>
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{" "}
<AnchorLink href="https://azure.microsoft.com/en-gb/products/app-service/static">
Static Web App
</AnchorLink>{" "}
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.
</Text>,
<Text>
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.
</Text>,
<Text>
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{" "}
<AnchorLink href="https://azure.microsoft.com/en-gb/products/azure-sql/database">
Azure SQL database
</AnchorLink>{" "}
where I'll document the full process of writing and deploying this app
so check back again soon.
</Text>,
],
},
];
export default function AboutTabs() { export default function AboutTabs() {
return ( return (
@@ -9,56 +160,35 @@ export default function AboutTabs() {
<div className="-mx-4 flex overflow-x-auto sm:mx-0"> <div className="-mx-4 flex overflow-x-auto sm:mx-0">
<div className="flex-auto border-b border-gray-200 px-4 sm:px-0"> <div className="flex-auto border-b border-gray-200 px-4 sm:px-0">
<Tab.List className="-mb-px flex space-x-8"> <Tab.List className="-mb-px flex space-x-8">
<Tab {tabs.map((tab) => (
className={({ selected }) => <Tab
buildClassNames( key={tab.tabName}
selected className={({ selected }) =>
? "border-gray-300 text-gray-200" buildClassNames(
: "border-transparent hover:border-gray-300 hover:text-gray-200", selected
"whitespace-nowrap border-b-2 py-6 text-sm font-medium" ? "border-gray-300 text-gray-200"
) : "border-transparent hover:border-gray-300 hover:text-gray-200",
} "whitespace-nowrap border-b-4 py-6 text-sm font-medium"
> )
Front-end }
</Tab> >
<Tab {tab.tabName}
className={({ selected }) => </Tab>
buildClassNames( ))}
selected
? "border-gray-300 text-gray-200"
: "border-transparent hover:border-gray-300 hover:text-gray-200",
"whitespace-nowrap border-b-2 py-6 text-sm font-medium"
)
}
>
Back-end
</Tab>
<Tab
className={({ selected }) =>
buildClassNames(
selected
? "border-gray-300 text-gray-200"
: "border-transparent hover:border-gray-300 hover:text-gray-200",
"whitespace-nowrap border-b-2 py-6 text-sm font-medium"
)
}
>
Hosting
</Tab>
</Tab.List> </Tab.List>
</div> </div>
</div> </div>
<Tab.Panels as={Fragment}> <Tab.Panels as={Fragment}>
<Tab.Panel className="space-y-16 pt-10"> {tabs.map((tab) => (
<Subtitle>Front-end</Subtitle> <Tab.Panel key={tab.tabName} className="pt-10">
</Tab.Panel> {tab.title}
<Tab.Panel className="space-y-16 pt-10"> <p className="font-bold text-lg my-4">Tech: {tab.subtitle}</p>
<Subtitle>Back-end</Subtitle> {tab.content.map((content, index) => (
</Tab.Panel> <Fragment key={index}>{content}</Fragment>
<Tab.Panel className="space-y-16 pt-10"> ))}
<Subtitle>Hosting</Subtitle> </Tab.Panel>
</Tab.Panel> ))}
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
); );

View File

@@ -7,7 +7,12 @@ interface Props {
className?: string | null; className?: string | null;
} }
export default function Link({ children, href, target, className }: Props) { export default function AnchorLink({
children,
href,
target,
className,
}: Props) {
const defaultStyles = "underline underline-offset-2 hover:underline-offset-4"; const defaultStyles = "underline underline-offset-2 hover:underline-offset-4";
const styles = buildClassNames(className ? className : "", defaultStyles); const styles = buildClassNames(className ? className : "", defaultStyles);

View File

@@ -1,5 +1,5 @@
import { FaRegPaperPlane } from "react-icons/fa6"; import { FaRegPaperPlane } from "react-icons/fa6";
import Link from "./Link"; import AnchorLink from "./AnchorLink";
import TextInput from "./TextInput"; import TextInput from "./TextInput";
import TextAreaInput from "./TextAreaInput"; import TextAreaInput from "./TextAreaInput";
import Button from "./Button"; import Button from "./Button";
@@ -37,13 +37,19 @@ export default function ContactForm() {
<div className="mt-4"> <div className="mt-4">
<small> <small>
This site is protected by reCAPTCHA and the Google{" "} This site is protected by reCAPTCHA and the Google{" "}
<Link href="https://policies.google.com/privacy" target="_blank"> <AnchorLink
href="https://policies.google.com/privacy"
target="_blank"
>
Privacy Policy Privacy Policy
</Link>{" "} </AnchorLink>{" "}
and{" "} and{" "}
<Link href="https://policies.google.com/terms" target="_blank"> <AnchorLink
href="https://policies.google.com/terms"
target="_blank"
>
Terms of Service Terms of Service
</Link>{" "} </AnchorLink>{" "}
apply. apply.
</small> </small>
</div> </div>

View File

@@ -0,0 +1,15 @@
import { ReactElement } from "react";
import buildClassNames from "../helpers/cssClassFormatter";
import { ListItemProps } from "./ListItem";
interface Props {
className?: string | null;
children: ReactElement<ListItemProps> | Array<ReactElement<ListItemProps>>;
}
export default function List({ className, children }: Props) {
const defaultStyles = "list-disc pl-8 space-y-2";
const styles = buildClassNames(className ? className : "", defaultStyles);
return <ul className={styles}>{children}</ul>;
}

View File

@@ -0,0 +1,9 @@
import { ReactNode } from "react";
export interface ListItemProps {
children: ReactNode;
}
export default function ListItem({ children }: ListItemProps) {
return <li>{children}</li>;
}

View File

@@ -1,12 +1,13 @@
import { ReactNode } from "react";
import buildClassNames from "../helpers/cssClassFormatter"; import buildClassNames from "../helpers/cssClassFormatter";
interface Props { interface Props {
children: string; children: ReactNode;
className?: string | null; className?: string | null;
} }
export default function Subtitle({ children, className }: Props) { export default function Subtitle({ children, className }: Props) {
const defaultStyles = "text-2xl py-4 font-semibold"; const defaultStyles = "flex items-center text-2xl py-4 font-semibold";
const styles = buildClassNames(className ? className : "", defaultStyles); const styles = buildClassNames(className ? className : "", defaultStyles);
return <h2 className={styles}>{children}</h2>; return <h2 className={styles}>{children}</h2>;

View File

@@ -7,7 +7,7 @@ interface Props {
} }
export default function Text({ children, className }: Props) { export default function Text({ children, className }: Props) {
const defaultStyles = "text-lg py-2"; const defaultStyles = "text-lg py-3";
const styles = buildClassNames(className ? className : "", defaultStyles); const styles = buildClassNames(className ? className : "", defaultStyles);
return <p className={styles}>{children}</p>; return <p className={styles}>{children}</p>;

View File

@@ -1,5 +1,5 @@
import AboutTabs from "../components/AboutTabs"; import AboutTabs from "../components/AboutTabs";
import Link from "../components/Link"; import AnchorLink from "../components/AnchorLink";
import Text from "../components/Text"; import Text from "../components/Text";
import Title from "../components/Title"; import Title from "../components/Title";
@@ -11,7 +11,10 @@ export default function AboutPage() {
Below is an overview of how this simple app is made and what 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 technologies are used. If you'd like to dive straight in, the full
project is available on my{" "} project is available on my{" "}
<Link href="https://github.com/bdfin/my-portfolio">GitHub</Link>. <AnchorLink href="https://github.com/bdfin/my-portfolio">
GitHub
</AnchorLink>
.
</Text> </Text>
<Text> <Text>
I'm planning to integrate a simple blog as part of this app that will I'm planning to integrate a simple blog as part of this app that will

View File

@@ -1,4 +1,4 @@
import Link from "../components/Link"; import AnchorLink from "../components/AnchorLink";
import Subtitle from "../components/Subtitle"; import Subtitle from "../components/Subtitle";
import Text from "../components/Text"; import Text from "../components/Text";
import Title from "../components/Title"; import Title from "../components/Title";
@@ -19,7 +19,7 @@ export default function HomePage() {
<Text> <Text>
I've worked with businesses at all sizes and stages and I'm currently 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{" "} heading up the tech as CTO at a cool startup called{" "}
<Link href="https://unhurdmusic.com">un:hurd music</Link>. <AnchorLink href="https://unhurdmusic.com">un:hurd music</AnchorLink>.
</Text> </Text>
</> </>
); );