Add about page
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
15
src/Client/src/components/List.tsx
Normal file
15
src/Client/src/components/List.tsx
Normal 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>;
|
||||||
|
}
|
||||||
9
src/Client/src/components/ListItem.tsx
Normal file
9
src/Client/src/components/ListItem.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface ListItemProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListItem({ children }: ListItemProps) {
|
||||||
|
return <li>{children}</li>;
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user