Compare commits
10 Commits
36b1224127
...
fad16cb46f
| Author | SHA1 | Date | |
|---|---|---|---|
| fad16cb46f | |||
|
|
014fc042a0 | ||
|
|
b8e1bf8467 | ||
|
|
e4a11cadab | ||
|
|
eae66518c8 | ||
|
|
3eb1798972 | ||
|
|
9e282f7ce5 | ||
|
|
fb438c8287 | ||
|
|
fbec8a00fd | ||
|
|
64e0b88a5e |
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 5000
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["src/BlazorApp/BlazorApp.csproj", "src/BlazorApp/"]
|
||||
RUN dotnet restore "src/BlazorApp/BlazorApp.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/BlazorApp"
|
||||
RUN dotnet build "./BlazorApp.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./BlazorApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
# Install curl for health checks
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=publish /app/publish .
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:5000/health || exit 1
|
||||
|
||||
ENTRYPOINT ["dotnet", "BlazorApp.dll"]
|
||||
@@ -5,10 +5,23 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Components\Shared\Icon.razor"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.15.0-beta.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
<footer>
|
||||
<div class="footer-container">
|
||||
<div class="footer-content">
|
||||
<SocialIcons/>
|
||||
<a href="/privacy-policy"
|
||||
class="footer-link">Privacy Policy</a>
|
||||
|
||||
<p class="footer-text">
|
||||
© @DateTime.Now.Year Beau Findlay. All rights reserved.
|
||||
</p>
|
||||
|
||||
<SocialIcons/>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="desktop-nav">
|
||||
<NavLink href="/experience">Experience</NavLink>
|
||||
<NavLink href="/experience">My Experience</NavLink>
|
||||
<NavLink href="/about">This app</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
<div class="mobile-menu-body">
|
||||
<div class="mobile-nav-links">
|
||||
<NavLink href="/experience">Experience</NavLink>
|
||||
<NavLink href="/experience">My Experience</NavLink>
|
||||
<NavLink href="/about">This App</NavLink>
|
||||
</div>
|
||||
<div class="mobile-social-divider">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</Text>
|
||||
|
||||
<section>
|
||||
<Subtitle>App</Subtitle>
|
||||
<Subtitle>The App</Subtitle>
|
||||
|
||||
<Text>
|
||||
This app was originally made using
|
||||
@@ -22,8 +22,17 @@
|
||||
<AnchorLink Href="https://react.dev/">React</AnchorLink>
|
||||
with
|
||||
<AnchorLink Href="https://www.typescriptlang.org/">TypeScript</AnchorLink>
|
||||
as a learning exercise. I've now migrated it back to .NET Blazor to take advantage of static server-side
|
||||
rendering for maximum performance and to remove unnecessary dependencies on large JS and CSS libraries.
|
||||
as a learning exercise. I've now migrated it back to .NET Blazor to take advantage of
|
||||
<AnchorLink Href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-10.0#static-server-side-rendering-static-ssr">
|
||||
SSR (static server-side rendering)
|
||||
</AnchorLink>
|
||||
for maximum performance and to remove unnecessary dependencies on large JS and CSS libraries.
|
||||
</Text>
|
||||
<Text>
|
||||
Although SSR sacrifices client-side interactivity, for simple sites like this it means that page loads are
|
||||
practically instant and appear to function similarly to SPAs (the browser doesn't reload on navigations, new
|
||||
content is simply swapped in) but without the initial large download of the full application and it's
|
||||
dependencies.
|
||||
</Text>
|
||||
<Text>
|
||||
This version uses pure vanilla CSS with CSS variables for theming, eliminating all JavaScript and external
|
||||
@@ -35,7 +44,32 @@
|
||||
<Subtitle>Hosting & Deployment</Subtitle>
|
||||
|
||||
<Text>
|
||||
TODO: This section
|
||||
When this app was written in Blazor WASM and then React (both client-side-only technologies) it was hosted on an
|
||||
<AnchorLink Href="https://learn.microsoft.com/en-us/azure/static-web-apps/overview">Azure Static Web App
|
||||
</AnchorLink>
|
||||
and deployed via
|
||||
<AnchorLink Href="https://github.com/features/actions">GitHub Actions</AnchorLink>. This provided an easy and cost effective way to get something out there without worrying too much about the
|
||||
infrastructure. With this being a personal project I needed to keep the costs minimal and even though I would've
|
||||
preferred a server to work with there wasn't many free options.
|
||||
</Text>
|
||||
<Text>
|
||||
I've always been a bit of a networking/homelab/server nerd so as soon as Cloudflare announced their
|
||||
<AnchorLink Href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/">
|
||||
Tunnels
|
||||
</AnchorLink>
|
||||
feature I decided to revisit this site, rebuild in a server-side technology and host it myself on a tiny,
|
||||
low-power
|
||||
<AnchorLink Href="https://www.raspberrypi.com/">RaspberryPi</AnchorLink>
|
||||
computer running a headless linux operating system.
|
||||
</Text>
|
||||
<Text>
|
||||
I setup the RaspberryPi with
|
||||
<AnchorLink Href="https://www.docker.com/">Docker</AnchorLink>
|
||||
and
|
||||
<AnchorLink Href="https://docs.docker.com/compose/">Docker Compose</AnchorLink>, built a Docker container for the .NET app to run from, copied it to my server and created a docker-compose.yml
|
||||
file to run it with the
|
||||
<AnchorLink Href="https://hub.docker.com/r/cloudflare/cloudflared">Cloudflare Tunnel</AnchorLink>
|
||||
docker image.
|
||||
</Text>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PageTitle>Beau Findlay - Experience</PageTitle>
|
||||
|
||||
<Title CssClass="text-center">Experience</Title>
|
||||
<Title CssClass="text-center">My Experience</Title>
|
||||
|
||||
<p class="text-center text-xl font-semibold mb-10 ">
|
||||
Software Engineer since 2018
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
I specialise in C#/.NET development and I've built systems that scale for hundreds-of-thousands of global users.
|
||||
</Text>
|
||||
<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
|
||||
<AnchorLink Href="https://unhurdmusic.com">un:hurd music</AnchorLink>.
|
||||
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
|
||||
<AnchorLink Href="https://unhurdmusic.com">un:hurd music</AnchorLink>
|
||||
.
|
||||
</Text>
|
||||
<Text>
|
||||
I believe in a privacy-first, information-focussed and performant internet. You won't find any trackers, analytics or the need for a cookie consent policy here.
|
||||
I believe in a privacy-first, information-focussed and performant internet. You won't find any trackers, analytics
|
||||
or the need for a cookie banner here.
|
||||
</Text>
|
||||
|
||||
53
src/BlazorApp/Components/Pages/PrivacyPolicy.razor
Normal file
53
src/BlazorApp/Components/Pages/PrivacyPolicy.razor
Normal file
@@ -0,0 +1,53 @@
|
||||
@page "/privacy-policy"
|
||||
|
||||
<PageTitle>Beau Findlay - Privacy Policy</PageTitle>
|
||||
|
||||
<Title CssClass="text-center mb-6">Privacy Policy</Title>
|
||||
<i>Last updated: 31 January 2025</i>
|
||||
|
||||
<section>
|
||||
<Subtitle>About this website</Subtitle>
|
||||
|
||||
<Text>This is a personal website operated by Beau Findlay.</Text>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle>Information collection</Subtitle>
|
||||
|
||||
<Text>This website does not collect, store, or process any personal data and I will never track you or collect your
|
||||
information.
|
||||
</Text>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle>Cookies</Subtitle>
|
||||
|
||||
<Text>
|
||||
This website uses Cloudflare for security and performance. Cloudflare may set cookies on your device under
|
||||
certain circumstances (such as when verifying you're not a bot):
|
||||
</Text>
|
||||
<ul class="mb-6">
|
||||
<li><em class="font-bold">__cf_bm</em> - Used for bot detection (expires after 30 minutes)</li>
|
||||
<li><em class="font-bold">cf_clearance</em> - Stores proof you passed a security challenge (expires within 24
|
||||
hours)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Text>
|
||||
These cookies are only set when needed, are strictly necessary for the website to function securely, and are not
|
||||
used for tracking or advertising. Under normal browsing, no cookies are set.
|
||||
</Text>
|
||||
|
||||
<Text>Cloudflare may process technical data to provide these services. See
|
||||
<AnchorLink Href="https://www.cloudflare.com/privacypolicy/">Cloudflare's Privacy Policy</AnchorLink>
|
||||
for details.
|
||||
</Text>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle>Contact</Subtitle>
|
||||
|
||||
<Text>If you have questions about this policy, you can reach me by <a href="mailto:me@beaufindlay.com"
|
||||
class="underline">email</a>.
|
||||
</Text>
|
||||
</section>
|
||||
@@ -1,17 +1,31 @@
|
||||
using BlazorApp.Components;
|
||||
using OpenTelemetry.Metrics;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents();
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics.AddAspNetCoreInstrumentation();
|
||||
metrics.AddPrometheusExporter();
|
||||
});
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
app.MapGet("/health", () => Results.Ok(new
|
||||
{
|
||||
status = "healthy",
|
||||
timestamp = DateTime.UtcNow
|
||||
}));
|
||||
|
||||
app.MapPrometheusScrapingEndpoint();
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", true);
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ body {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
@@ -37,6 +35,28 @@ p {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: var(--space-4);
|
||||
}
|
||||
|
||||
ul li {
|
||||
position: relative;
|
||||
padding-left: var(--space-6);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
ul li::before {
|
||||
content: ">";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-slate-400);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: 1.5;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: var(--color-slate-50);
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
|
||||
.mobile-menu-body {
|
||||
margin-top: var(--space-6);
|
||||
flex: 1 1 0%;
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
@@ -236,9 +236,7 @@
|
||||
margin-right: -0.75rem;
|
||||
display: block;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0.75rem;
|
||||
padding-top: var(--space-2);
|
||||
padding-bottom: var(--space-2);
|
||||
padding: var(--space-2) 0.75rem;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.75;
|
||||
@@ -268,13 +266,12 @@ body:has(.menu-toggle:checked) {
|
||||
.mobile-menu-content {
|
||||
max-width: 24rem;
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
border-left: 2px solid var(--color-slate-800);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.navbar .logo-container {
|
||||
flex: 1 1 0%;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.navbar .mobile-menu-button-container {
|
||||
@@ -307,12 +304,18 @@ body:has(.menu-toggle:checked) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.page-footer .footer-link {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-slate-50);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.page-footer .footer-text {
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.25;
|
||||
color: var(--color-slate-50);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -320,13 +323,7 @@ body:has(.menu-toggle:checked) {
|
||||
@media (min-width: 768px) {
|
||||
.page-footer .footer-content {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.page-footer .footer-text {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
24
src/Client/.gitignore
vendored
24
src/Client/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,30 +0,0 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||
@@ -1,40 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<meta
|
||||
property="description"
|
||||
content="I'm Beau. A software engineer, tech enthusianst and music lover."
|
||||
/>
|
||||
<meta property="author" content="Beau Findlay" />
|
||||
<meta property="og:title" content="Beau Findlay" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="I'm Beau. A software engineer, tech enthusianst and music lover"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://beaufindlay.com/logo.webp"
|
||||
/>
|
||||
<meta property="og:url" content="https://beaufindlay.com" />
|
||||
<title>Beau Findlay</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.webp" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4372
src/Client/package-lock.json
generated
4372
src/Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.1.0",
|
||||
"react-router-dom": "^6.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,137 +0,0 @@
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { Fragment, ReactNode } from "react";
|
||||
import { SiMicrosoftazure, SiReact } from "react-icons/si";
|
||||
import buildClassNames from "../helpers/cssClassFormatter";
|
||||
import AnchorLink from "./AnchorLink";
|
||||
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: "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 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() {
|
||||
return (
|
||||
<Tab.Group as="div" className="mt-4">
|
||||
<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">
|
||||
<Tab.List className="-mb-px flex space-x-8">
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.tabName}
|
||||
className={({ selected }) =>
|
||||
buildClassNames(
|
||||
selected
|
||||
? "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"
|
||||
)
|
||||
}
|
||||
>
|
||||
{tab.tabName}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tab.Panels as={Fragment}>
|
||||
{tabs.map((tab) => (
|
||||
<Tab.Panel key={tab.tabName} className="pt-10">
|
||||
{tab.title}
|
||||
<p className="font-bold text-lg my-4">Tech: {tab.subtitle}</p>
|
||||
{tab.content.map((content, index) => (
|
||||
<Fragment key={index}>{content}</Fragment>
|
||||
))}
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import buildClassNames from "../helpers/cssClassFormatter";
|
||||
|
||||
interface Props {
|
||||
children: string;
|
||||
href?: string;
|
||||
target?: "_blank" | "";
|
||||
className?: string | null;
|
||||
}
|
||||
|
||||
export default function AnchorLink({
|
||||
children,
|
||||
href,
|
||||
target,
|
||||
className,
|
||||
}: Props) {
|
||||
const defaultStyles = "underline underline-offset-2 hover:underline-offset-4";
|
||||
const styles = buildClassNames(className ? className : "", defaultStyles);
|
||||
|
||||
return (
|
||||
<a href={href} target={target} className={styles}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
type: "submit" | "button";
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function Button({ type, children, onClick }: Props) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
className="flex items-center justify-center border-0 ring-1 ring-inset ring-gray-300 bg-black px-3.5 py-2.5 text-sm font-semibold text-white shadow hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600 disabled:bg-gray-800 disabled:cursor-progress"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { FaRegPaperPlane } from "react-icons/fa6";
|
||||
import Text from "../components/Text";
|
||||
|
||||
export default function ContactMe() {
|
||||
return (
|
||||
<div className="mb-10 mt-12 text-center">
|
||||
<Text>If you think I can help with your project...</Text>
|
||||
<a
|
||||
href="mailto:me@beaufindlay.com"
|
||||
className="inline-flex items-center border-0 ring-1 ring-inset ring-gray-300 bg-black px-3.5 py-2.5 mt-2 text-sm font-semibold text-white shadow hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
|
||||
>
|
||||
Get in touch <FaRegPaperPlane className="ml-2" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import SocialIcons from "./SocialIcons";
|
||||
|
||||
export default function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="mt-auto">
|
||||
<div className="mx-auto py-8">
|
||||
<div className="md:flex md:items-center md:justify-between">
|
||||
<SocialIcons />
|
||||
<p className="mt-4 text-xs leading-5 text-gray-100 md:order-1 md:mt-0">
|
||||
© {currentYear} Beau Findlay. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
interface Props {
|
||||
htmlFor: string;
|
||||
children: string;
|
||||
}
|
||||
|
||||
export default function Label({ htmlFor, children }: Props) {
|
||||
return (
|
||||
<label htmlFor={htmlFor} className="block font-semibold leading-6">
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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>;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface ListItemProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function ListItem({ children }: ListItemProps) {
|
||||
return <li>{children}</li>;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import LoadingSpinner from "./LoadingSpinner";
|
||||
import logo from "../assets/logo.webp";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="bg-black font-mono text-slate-50 antialiased px-6 lg:px-8">
|
||||
<div className="flex flex-col min-h-screen mx-auto max-w-7xl items-center justify-center fade-in">
|
||||
<img className="h-20 w-auto" src={logo} alt="Logo" />
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
export default function LoadingSpinner() {
|
||||
return (
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-8 h-8 text-gray-500 animate-spin fill-white"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Dialog, Popover } from "@headlessui/react";
|
||||
import { useState } from "react";
|
||||
import { FaBars, FaXmark } from "react-icons/fa6";
|
||||
import { Link } from "react-router-dom";
|
||||
import logo from "../assets/logo.webp";
|
||||
import SocialIcons from "./SocialIcons";
|
||||
import NavLink from "./NavLink";
|
||||
|
||||
export default function NavBar() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="pt-6">
|
||||
<nav
|
||||
className="mx-auto flex max-w-7xl items-center justify-between"
|
||||
aria-label="Global"
|
||||
>
|
||||
<div className="flex lg:flex-1">
|
||||
<Link to="/" className="-m-1.5 p-1.5">
|
||||
<span className="sr-only">Beau Findlay</span>
|
||||
<img className="h-16 w-auto" src={logo} alt="Logo" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<FaBars className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<Popover.Group className="hidden lg:flex lg:gap-x-12">
|
||||
<NavLink to="/work">Work</NavLink>
|
||||
<NavLink to="/about">This App</NavLink>
|
||||
</Popover.Group>
|
||||
</nav>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="lg:hidden"
|
||||
open={mobileMenuOpen}
|
||||
onClose={setMobileMenuOpen}
|
||||
>
|
||||
<div className="fixed inset-0 z-10" />
|
||||
<Dialog.Panel className="fixed inset-y-0 right-0 z-10 bg-black w-full overflow-y-auto px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 text-white sm:border-l-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
to="/"
|
||||
className="-m-1.5 p-1.5"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Beau Findlay</span>
|
||||
<img className="h-16 w-auto" src={logo} alt="Logo" />
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 rounded-md p-2.5"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close menu</span>
|
||||
<FaXmark className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6 flow-root">
|
||||
<div className="-my-6 divide-y divide-gray-200/10">
|
||||
<div className="space-y-2 py-6">
|
||||
<NavLink
|
||||
to="/work"
|
||||
className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Work
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/about"
|
||||
className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
This App
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<SocialIcons size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { NavLink as ReactNavLink } from "react-router-dom";
|
||||
|
||||
interface Props {
|
||||
children: string;
|
||||
to: string;
|
||||
className?: string | null;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function NavLink({ children, to, className, onClick }: Props) {
|
||||
const defaultStyles = "text-base font-semibold leading-6 hover:text-gray-300";
|
||||
const styles = className ? className : defaultStyles;
|
||||
|
||||
return (
|
||||
<ReactNavLink
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
className={({ isActive }) =>
|
||||
isActive ? `${styles} underline underline-offset-4` : styles
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ReactNavLink>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { FaEnvelope, FaGithub, FaLinkedin } from "react-icons/fa6";
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function SocialIcons({ size = 20 }: Props) {
|
||||
const socialIcons = [
|
||||
{
|
||||
name: "GitHub",
|
||||
href: "https://github.com/bdfin",
|
||||
icon: <FaGithub size={size} />,
|
||||
},
|
||||
{
|
||||
name: "LinkedIn",
|
||||
href: "https://www.linkedin.com/in/beau-findlay/",
|
||||
icon: <FaLinkedin size={size} />,
|
||||
},
|
||||
{
|
||||
name: "Email",
|
||||
href: "mailto:me@beaufindlay.com",
|
||||
icon: <FaEnvelope size={size} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex space-x-6 md:order-2">
|
||||
{socialIcons.map((socialIcon) => (
|
||||
<a
|
||||
key={socialIcon.name}
|
||||
href={socialIcon.href}
|
||||
className="text-gray-100 hover:text-gray-300"
|
||||
>
|
||||
<span className="sr-only">{socialIcon.name}</span>
|
||||
{socialIcon.icon}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import buildClassNames from "../helpers/cssClassFormatter";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string | null;
|
||||
}
|
||||
|
||||
export default function Subtitle({ children, className }: Props) {
|
||||
const defaultStyles = "flex items-center text-2xl py-4 font-semibold";
|
||||
const styles = buildClassNames(className ? className : "", defaultStyles);
|
||||
|
||||
return <h2 className={styles}>{children}</h2>;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { FaDatabase, FaDocker, FaReact } from "react-icons/fa6";
|
||||
import { SiBlazor, SiCsharp, SiMicrosoftazure } from "react-icons/si";
|
||||
import buildClassNames from "../helpers/cssClassFormatter";
|
||||
|
||||
const iconSize = 34;
|
||||
const iconCss = "mx-auto";
|
||||
|
||||
const techIcons = [
|
||||
{
|
||||
name: "C#/.NET",
|
||||
icon: <SiCsharp size={iconSize} className={iconCss} />,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Azure",
|
||||
icon: <SiMicrosoftazure size={iconSize} className={iconCss} />,
|
||||
},
|
||||
{
|
||||
name: "Blazor",
|
||||
icon: <SiBlazor size={iconSize} className={iconCss} />,
|
||||
},
|
||||
{
|
||||
name: "React",
|
||||
icon: <FaReact size={iconSize} className={iconCss} />,
|
||||
},
|
||||
{
|
||||
name: "Databases",
|
||||
icon: <FaDatabase size={iconSize} className={iconCss} />,
|
||||
},
|
||||
{
|
||||
name: "Docker",
|
||||
icon: <FaDocker size={iconSize} className={iconCss} />,
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
className?: string | null;
|
||||
}
|
||||
|
||||
export default function TechIcons({ className }: Props) {
|
||||
const defaultStyles = "mx-auto max-w-4xl";
|
||||
const styles = buildClassNames(className ? className : "", defaultStyles);
|
||||
|
||||
return (
|
||||
<div className={styles}>
|
||||
<p className="text-xl text-center mb-10 font-semibold">
|
||||
Tech i'm working with at the moment:
|
||||
</p>
|
||||
<div className="flex flex-col md:flex-row md:justify-evenly space-y-10 md:space-y-0 text-center mx-auto mt-4">
|
||||
{techIcons.map((techIcon, index) => (
|
||||
<div key={index}>
|
||||
{techIcon.icon}
|
||||
<p className="mt-2 text-sm">{techIcon.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import buildClassNames from "../helpers/cssClassFormatter";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string | null;
|
||||
}
|
||||
|
||||
export default function Text({ children, className }: Props) {
|
||||
const defaultStyles = "text-lg py-3";
|
||||
const styles = buildClassNames(className ? className : "", defaultStyles);
|
||||
|
||||
return <p className={styles}>{children}</p>;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
interface Props {
|
||||
id: string;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
export default function TextAreaInput({ id, rows = 4 }: Props) {
|
||||
return (
|
||||
<textarea
|
||||
id={id}
|
||||
rows={rows}
|
||||
className="block w-full text-lg border-0 px-3.5 py-2 bg-black shadow ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600"
|
||||
></textarea>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "text" | "email";
|
||||
}
|
||||
|
||||
export default function TextInput({ id, type }: Props) {
|
||||
return (
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
className="block w-full text-lg border-0 px-3.5 py-2 bg-black shadow ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import buildClassNames from "../helpers/cssClassFormatter";
|
||||
|
||||
interface Props {
|
||||
children: string;
|
||||
className?: string | null;
|
||||
}
|
||||
|
||||
export default function Title({ children, className }: Props) {
|
||||
const defaultStyles = "text-4xl py-4";
|
||||
const styles = buildClassNames(className ? className : "", defaultStyles);
|
||||
|
||||
return <h1 className={styles}>{children}</h1>;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import AnchorLink from "./AnchorLink";
|
||||
import Text from "./Text";
|
||||
|
||||
interface WorkTimelineItem {
|
||||
startDate: string;
|
||||
endDate?: string | null;
|
||||
title: string;
|
||||
companyName: string;
|
||||
companyUrl: string;
|
||||
content: string[];
|
||||
}
|
||||
|
||||
const workTimelineItems: WorkTimelineItem[] = [
|
||||
{
|
||||
startDate: "September 2021",
|
||||
title: "CTO",
|
||||
companyName: "un:hurd music",
|
||||
companyUrl: "https://unhurdmusic.com",
|
||||
content: [
|
||||
"As one of the founding developers at un:hurd music and now Chief Technology Officer, I built and scaled un:hurd's back-end and cloud infrastructure that serves automated marketing soloutions for tens-of-thousands of artists and musicians.",
|
||||
"I lead a small but incredibly talented multi-diciplinary team building on the Azure cloud using a .NET backend, React web front-end and a Swift native iOS app.",
|
||||
],
|
||||
},
|
||||
{
|
||||
startDate: "August 2020",
|
||||
endDate: "September 2021",
|
||||
title: "Software Development Lead",
|
||||
companyName: "Vouch",
|
||||
companyUrl: "https://vouch.co.uk/",
|
||||
content: [
|
||||
"At Vouch I lead the backend build of a new version of their tenant referencing software - an AI enhanced chat-bot based system utlising Azure Cognitive Services and various supporting serverless APIs written in .NET Core and hosted on Microsoft Azure.",
|
||||
],
|
||||
},
|
||||
{
|
||||
startDate: "May 2020",
|
||||
endDate: "July 2020",
|
||||
title: "Software Developer",
|
||||
companyName: "Paragon ID",
|
||||
companyUrl: "https://www.paragon-id.com/en",
|
||||
content: [
|
||||
"I joined Paragon ID on a short-term contract where I wrote and deployed two key projects: A complex dashboard for a large construction equipment manufacturer to track assets across various manufacturing stages and a medical assets tracking dashboard deployed and used in multiple hospitals across the UK.",
|
||||
],
|
||||
},
|
||||
{
|
||||
startDate: "July 2019",
|
||||
endDate: "May 2020",
|
||||
title: "Software Developer",
|
||||
companyName: "Osborne Technologies",
|
||||
companyUrl: "https://www.osbornetechnologies.co.uk/",
|
||||
content: [
|
||||
"I joined Osborne Technologies as the only cloud cloud-specialist and lead a project creating the first web-based version of their flag ship visitor management software utilising ASP.NET Core MVC and Microsoft SQL Server on the Microsoft Azure cloud.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function WorkTimeline() {
|
||||
return (
|
||||
<ol className="relative border-s border-gray-600">
|
||||
{workTimelineItems.map((item, index) => (
|
||||
<li key={index} className="mb-10 ms-4">
|
||||
<div className="absolute w-3 h-3 rounded-full mt-1.5 -start-1.5 borderborder-gray-900 bg-gray-600"></div>
|
||||
<time className="mb-1 text-sm font-normal leading-none text-gray-400">
|
||||
{item.startDate} - {item.endDate ? item.endDate : "Present"}
|
||||
</time>
|
||||
<h3 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{item.title} @{" "}
|
||||
<AnchorLink href={item.companyUrl}>{item.companyName}</AnchorLink>
|
||||
</h3>
|
||||
{item.content.map((content, index) => (
|
||||
<Text key={index}>{content}</Text>
|
||||
))}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function buildClassNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.fade-in {
|
||||
animation: fadeInAnimation ease 1s;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInAnimation {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track {
|
||||
background: white;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb {
|
||||
background-color: black;
|
||||
border: 1px solid white;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import router from "./routes.tsx";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -1,22 +0,0 @@
|
||||
import AboutTabs from "../components/AboutTabs";
|
||||
import AnchorLink from "../components/AnchorLink";
|
||||
import Text from "../components/Text";
|
||||
import Title from "../components/Title";
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
<Title className="text-center pb-4">This App</Title>
|
||||
<Text>
|
||||
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{" "}
|
||||
<AnchorLink href="https://github.com/bdfin/my-portfolio">
|
||||
GitHub
|
||||
</AnchorLink>
|
||||
.
|
||||
</Text>
|
||||
<AboutTabs />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { isRouteErrorResponse, useRouteError } from "react-router-dom";
|
||||
import Footer from "../components/Footer";
|
||||
import NavBar from "../components/NavBar";
|
||||
|
||||
export default function ErrorPage() {
|
||||
const error = useRouteError();
|
||||
|
||||
let errorCode = "Oops";
|
||||
let errorTitle = "Something went wrong.";
|
||||
let errorMessage = "This error has been automatically logged.";
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
errorCode = "404";
|
||||
errorTitle = "Page not found";
|
||||
errorMessage = "Sorry, this page dosen't exist.";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-black font-mono text-slate-50 antialiased px-6 lg:px-8">
|
||||
<div className="flex flex-col min-h-screen mx-auto max-w-7xl fade-in">
|
||||
<NavBar />
|
||||
<main className="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8">
|
||||
<div className="text-center">
|
||||
<p className="text-base font-semibold ">{errorCode}</p>
|
||||
<h1 className="mt-4 text-4xl font-bold tracking-tight">
|
||||
{errorTitle}
|
||||
</h1>
|
||||
<p className="mt-6 text-base leading-7 ">{errorMessage}</p>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import AnchorLink from "../components/AnchorLink";
|
||||
import TechIcons from "../components/TechIcons";
|
||||
import Text from "../components/Text";
|
||||
import Title from "../components/Title";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Title className="text-center mb-6">Hi, I'm Beau.</Title>
|
||||
<Text>
|
||||
I'm a UK-based software engineer and I love building cool stuff.
|
||||
</Text>
|
||||
<Text>
|
||||
I mostly specialise in back-end C#/.NET development and I've built
|
||||
systems that scale for hundreds-of-thousands of global users.
|
||||
</Text>
|
||||
<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{" "}
|
||||
<AnchorLink href="https://unhurdmusic.com">un:hurd music</AnchorLink>.
|
||||
</Text>
|
||||
|
||||
<TechIcons className="mt-28" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import NavBar from "../components/NavBar";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div className="bg-black font-mono text-slate-50 antialiased px-6 lg:px-10">
|
||||
<div className="flex flex-col min-h-screen mx-auto max-w-7xl fade-in">
|
||||
<NavBar />
|
||||
<div className="flex-1 py-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import ContactMe from "../components/ContactMe";
|
||||
import Title from "../components/Title";
|
||||
import WorkTimeline from "../components/WorkTimeline";
|
||||
|
||||
export default function WorkPage() {
|
||||
return (
|
||||
<>
|
||||
<Title className="text-center mb-4">Work</Title>
|
||||
<p className="text-center text-2xl font-semibold mb-12">
|
||||
Freelance Software Engineer since 2018
|
||||
</p>
|
||||
<WorkTimeline />
|
||||
<ContactMe />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import AboutPage from "./pages/AboutPage";
|
||||
import ErrorPage from "./pages/ErrorPage";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import Layout from "./pages/Layout";
|
||||
import WorkPage from "./pages/WorkPage";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Layout />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{ index: true, element: <HomePage /> },
|
||||
{ path: "work", element: <WorkPage /> },
|
||||
{ path: "about", element: <AboutPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export default router;
|
||||
1
src/Client/src/vite-env.d.ts
vendored
1
src/Client/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"navigationFallback": {
|
||||
"rewrite": "index.html",
|
||||
"exclude": ["/static/media/*.{png,jpg,jpeg,gif,bmp}", "/static/css/*"]
|
||||
},
|
||||
"mimeTypes": {
|
||||
".json": "text/json"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user