Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cec6e7758 | ||
|
|
a1c84ccc58 | ||
|
|
75a16af0c4 | ||
|
|
deb370d5af | ||
|
|
8508b679c0 | ||
|
|
fa7e1a5e09 | ||
|
|
7c40c82296 | ||
|
|
7ad84c3ddb | ||
|
|
1e94089b24 | ||
|
|
4771990d3c | ||
|
|
d9df8700b4 | ||
|
|
14173ff9ad | ||
|
|
f09b403282 | ||
|
|
cf19bed083 | ||
|
|
59fa92ef2f |
61
README.md
61
README.md
|
|
@ -1,36 +1,47 @@
|
|||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# Portfolio
|
||||
|
||||
## Getting Started
|
||||
A high-performance, containerized professional portfolio and R&D lab built with **Next.js 15**, **Tailwind CSS**, and **TypeScript**. This project serves as both a public showcase and a dynamic bridge to a private **Forgejo** instance for real-time development tracking.
|
||||
|
||||
First, run the development server:
|
||||
## 🏗️ Architecture Summary
|
||||
|
||||
- **Infrastructure**: Hosted on a **Hetzner** cloud node.
|
||||
- **Containerization**: Fully Dockerized for **linux/amd64** architectures.
|
||||
- **CI/CD**: Automated build and deployment via **Woodpecker CI**.
|
||||
- **CDN**: Assets and media served via **Bunny CDN** for global performance.
|
||||
- **Data Layer**: Dynamic changelog fetching from private repositories via **Forgejo API**.
|
||||
|
||||
## 🛠️ Technical Stack
|
||||
|
||||
- **Framework**: Next.js 15 (App Router).
|
||||
- **Styling**: Tailwind CSS with a monospace "Technical Lab" aesthetic.
|
||||
- **Icons**: Lucide React.
|
||||
- **Deployment**: Docker Compose with `force-dynamic` runtime environment injection.
|
||||
|
||||
## 🚀 Deployment & Build
|
||||
|
||||
The project utilizes a specialized build process to ensure compatibility with the production Hetzner node environment:
|
||||
|
||||
### Manual Build
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
docker buildx build --platform linux/amd64 -t portfolio:latest .
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
### Automated Pipeline
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
Deployments are triggered via Git Releases. The Woodpecker pipeline executes the following steps:
|
||||
-Build: Compiles the Next.js application for linux/amd64.
|
||||
-Push: Uploads the image to a private container registry.
|
||||
-Deploy: Re-creates the container on the Hetzner node, injecting the FORGEJO_TOKEN from a secure .env file at runtime.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
## 🧪 The_Forge Integration
|
||||
|
||||
## Learn More
|
||||
"The Forge" is a dynamic project log system that provides a glimpse into active R&D cycles.
|
||||
-Metadata: Configured via data/forge.ts for project-specific details.
|
||||
-Real-time Logs: Fetched server-side from private changelog.md files.
|
||||
-Status: Includes a conditional ACTIVE_STREAM pulse based on commit recency.
|
||||
-Performance: Utilizes Next.js data caching with a 1-hour revalidation window.
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
---
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
Built for performance and technical transparency.
|
||||
|
|
|
|||
381
app/cv/page.tsx
Normal file
381
app/cv/page.tsx
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
"use client";
|
||||
|
||||
import PageLayout from "@/components/PageLayout";
|
||||
import { Mail, Globe, MapPin, ExternalLink, Server, Zap } from "lucide-react";
|
||||
|
||||
export default function CVPage() {
|
||||
return (
|
||||
<PageLayout backLink="/" maxWidth="5xl">
|
||||
{/* Hide the print button itself during printing */}
|
||||
<div className="flex justify-end mb-4 no-print">
|
||||
<button
|
||||
onClick={() => window.print()}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-slate-800 hover:bg-orange-600 text-white text-xs font-mono tracking-widest uppercase transition-colors rounded-sm"
|
||||
>
|
||||
<Zap size={14} /> Print PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Container*/}
|
||||
<div className="bg-white text-slate-900 p-8 md:p-16 rounded-sm shadow-2xl print:shadow-none print:p-12 print:m-0 font-sans print:text-[12pt] leading-normal">
|
||||
<header className="border-b-4 border-slate-900 pb-8 print:pb-4 mb-8 print:mb-6 flex flex-col md:flex-row print:grid print:grid-cols-[1.5fr_1fr] justify-between items-start gap-6">
|
||||
<div className="flex flex-wrap items-center gap-4 sm:gap-6 mb-4 print:mb-2 print:flex-nowrap">
|
||||
<div className="shrink-0">
|
||||
<img
|
||||
src="/profile.jpg"
|
||||
alt="George A. Webberley"
|
||||
className="w-20 h-20 md:w-24 md:h-24 print:w-16 print:h-16 rounded-full object-cover border-2 border-slate-100 shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-3xl md:text-5xl print:text-3xl font-extrabold tracking-tighter mb-1 text-slate-900 lg:whitespace-nowrap print:whitespace-nowrap">
|
||||
George A. Webberley
|
||||
</h1>
|
||||
<p className="text-lg md:text-2xl print:text-[14pt] text-orange-600 font-bold uppercase tracking-tight lg:whitespace-nowrap print:whitespace-nowrap">
|
||||
Full Stack Engineer // Systems Architect
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Contact Info */}
|
||||
<div className="space-y-1 text-sm print:text-[9px] text-slate-500 font-mono text-right print:w-full print:flex print:flex-col print:items-end">
|
||||
<div className="flex items-center md:justify-end gap-2">
|
||||
<MapPin size={14} /> Copenhagen, Denmark
|
||||
</div>
|
||||
<div className="flex items-center md:justify-end gap-2">
|
||||
<Mail size={14} /> george@georgew.dev
|
||||
</div>
|
||||
<div className="flex items-center md:justify-end gap-2 text-orange-600 font-bold">
|
||||
<Globe size={14} /> georgew.dev
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 print:grid-cols-12 gap-8 print:gap-10">
|
||||
{/* Main Column */}
|
||||
<div className="lg:col-span-8 print:col-span-8 space-y-10 print:space-y-6">
|
||||
<section>
|
||||
<h2 className="text-xs uppercase tracking-[0.2em] font-black text-slate-400 mb-4 print:mb-2 border-b border-slate-100 pb-2">
|
||||
Professional Summary
|
||||
</h2>
|
||||
<p className="text-md print:text-sm leading-relaxed text-slate-700">
|
||||
After 5 years as a dental surgeon, a serendipitous broken leg
|
||||
led me to discover that software engineering perfectly suits my
|
||||
analytical mind. Now an MSc (Distinction) graduate and Senior
|
||||
Engineer, my core expertise lies in the entire product
|
||||
lifecycle. I’m less about a specific niche and more about the
|
||||
whole stack. I enjoy the challenge of creating a clean frontend,
|
||||
connecting a stable backend API, and building the infrastructure
|
||||
that keeps it all running. I'm always looking for the most
|
||||
efficient way to get a project from 'concept' to
|
||||
'shipped'.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xs uppercase tracking-[0.2em] font-black text-slate-400 mb-6 print:mb-3 border-b border-slate-100 pb-2">
|
||||
Engineering Experience
|
||||
</h2>
|
||||
<div className="space-y-10 print:space-y-6">
|
||||
{/* Brain+ Section */}
|
||||
<div className="break-inside-avoid">
|
||||
<div className="flex justify-between items-baseline mb-1">
|
||||
<h3 className="text-xl print:text-base font-bold italic text-slate-900">
|
||||
Brain+, Copenhagen
|
||||
</h3>
|
||||
<span className="text-sm print:text-xs font-mono font-bold text-orange-600 uppercase">
|
||||
2022 — Present
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-500 font-bold mb-3 print:mb-1 italic text-sm print:text-xs">
|
||||
Technical Lead & Senior Full Stack Developer
|
||||
</p>
|
||||
<ul className="list-disc ml-5 space-y-2 text-slate-600 text-sm print:text-[12px] print:leading-snug">
|
||||
<li>
|
||||
<strong>Product Ownership:</strong> I act as the bridge
|
||||
between technical and product teams; I handle the full
|
||||
product lifecycle from initial design and sprint planning
|
||||
to final demos.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Infrastructure:</strong> Architected a resilient
|
||||
multi-cloud setup focusing on high availability,
|
||||
scalability, and security. I leveraged container
|
||||
orchestration (K8s), modern load balancers, and VPC
|
||||
security policies all managed through IaC (Terraform) to
|
||||
ensure 24/7 reliability.
|
||||
</li>
|
||||
<li>
|
||||
<strong>DevOps & Automation:</strong> Built automated
|
||||
CI/CD pipelines using GitHub Actions and Fastlane for
|
||||
mobile releases. I utilize Kubectl and custom Makefiles to
|
||||
streamline cluster management and standardize local
|
||||
development environments.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Full Stack Delivery:</strong> Scaled core features
|
||||
using Ruby on Rails REST API services and Flask, while
|
||||
managing the rigorous documentation and release processes
|
||||
required for high-stakes medical compliance.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Other Roles */}
|
||||
<div className="space-y-8 print:space-y-4">
|
||||
{/* StageUp */}
|
||||
<div className="break-inside-avoid">
|
||||
<div className="flex justify-between items-baseline mb-1">
|
||||
<h4 className="text-lg print:text-sm font-bold italic text-slate-800">
|
||||
StageUp, Cardiff
|
||||
</h4>
|
||||
<span className="text-xs font-mono text-slate-400 uppercase">
|
||||
2021 — 2022
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm print:text-xs text-slate-600 mb-3 print:mb-1.5 leading-relaxed">
|
||||
Delivered new features and backend logic for a startup
|
||||
platform, while maintaining the GCP infrastructure and
|
||||
deployment pipelines.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
"Angular",
|
||||
"Node.js",
|
||||
"PostgreSQL",
|
||||
"Terraform",
|
||||
"Docker",
|
||||
"GCP",
|
||||
].map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="text-[9px] print:text-[8px] font-bold px-1.5 py-0.5 bg-slate-50 border border-slate-200 text-slate-500 rounded-sm uppercase"
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Startemup */}
|
||||
<div className="break-inside-avoid">
|
||||
<div className="flex justify-between items-baseline mb-1">
|
||||
<h4 className="text-lg print:text-sm font-bold italic text-slate-800">
|
||||
Startemup, Ontario
|
||||
</h4>
|
||||
<span className="text-xs font-mono text-slate-400 uppercase">
|
||||
2021 — 2023
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm print:text-xs text-slate-600 mb-3 print:mb-1.5 leading-relaxed">
|
||||
Technical troubleshooter for complex WordPress
|
||||
customizations requiring bespoke PHP logic and deep
|
||||
performance optimization.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
"PHP",
|
||||
"WordPress",
|
||||
"Performance Optimization",
|
||||
"Bespoke Logic",
|
||||
].map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="text-[9px] print:text-[8px] font-bold px-1.5 py-0.5 bg-slate-50 border border-slate-200 text-slate-500 rounded-sm uppercase"
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ratoong */}
|
||||
<div className="break-inside-avoid">
|
||||
<div className="flex justify-between items-baseline mb-1">
|
||||
<h4 className="text-lg print:text-sm font-bold italic text-slate-800">
|
||||
Ratoong, Copenhagen
|
||||
</h4>
|
||||
<span className="text-xs font-mono text-slate-400 uppercase">
|
||||
2020 — 2022
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm print:text-xs text-slate-600 mb-3 print:mb-1.5 leading-relaxed">
|
||||
Leading the end-to-end development of a functional SPA,
|
||||
moving from initial UI designs to a live production
|
||||
environment.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{["Angular", "Firebase", "GCP", "SPA Architecture"].map(
|
||||
(t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="text-[9px] print:text-[8px] font-bold px-1.5 py-0.5 bg-slate-50 border border-slate-200 text-slate-500 rounded-sm uppercase"
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="lg:col-span-4 print:col-span-4 space-y-10 print:space-y-6">
|
||||
<section>
|
||||
<h2 className="text-xs uppercase tracking-[0.2em] font-black text-slate-400 mb-6 print:mb-3 border-b border-slate-100 pb-2 text-slate-900">
|
||||
Technical Mastery
|
||||
</h2>
|
||||
<div className="space-y-6 print:space-y-4">
|
||||
<div>
|
||||
<p className="text-[10px] font-black uppercase mb-2 text-slate-500">
|
||||
Infrastructure & Ops
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
"Kubernetes",
|
||||
"Docker",
|
||||
"Terraform",
|
||||
"GCP",
|
||||
"OTC",
|
||||
"CI/CD",
|
||||
"VPC",
|
||||
].map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="px-2 py-0.5 bg-slate-900 text-white text-[10px] print:text-[8px] font-bold rounded-sm uppercase"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black uppercase mb-2 text-slate-500">
|
||||
Full Stack Engineering
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
"TypeScript",
|
||||
"Flutter",
|
||||
"Python",
|
||||
"React",
|
||||
"Angular",
|
||||
"Node.js",
|
||||
"Rails",
|
||||
"PostgreSQL",
|
||||
"NoSQL",
|
||||
].map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="px-2 py-0.5 bg-slate-100 text-slate-700 text-[10px] print:text-[8px] font-bold rounded-sm uppercase"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xs uppercase tracking-[0.2em] font-black text-slate-400 mb-6 print:mb-3 border-b border-slate-100 pb-2">
|
||||
Education
|
||||
</h2>
|
||||
<div className="space-y-4 print:space-y-2">
|
||||
<div>
|
||||
<p className="font-bold text-slate-900 text-sm print:text-xs">
|
||||
MSc Computer Science
|
||||
</p>
|
||||
<p className="text-[10px] text-orange-600 font-bold uppercase tracking-tighter">
|
||||
Distinction
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-400 italic">
|
||||
Univ. of Bristol
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-900 text-sm print:text-xs">
|
||||
Bachelor of Dental Surgery
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-400 italic">
|
||||
Univ. of Bristol (Merit)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xs uppercase tracking-[0.2em] font-black text-slate-400 mb-6 print:mb-3 border-b border-slate-100 pb-2 text-slate-900">
|
||||
Systems Tinkering
|
||||
</h2>
|
||||
<div className="space-y-6 print:space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Server size={14} className="text-orange-600" />
|
||||
<p className="font-bold text-slate-900 text-xs">
|
||||
Cloud-Hybrid Laboratory
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[11px] print:text-[10px] text-slate-600 leading-snug">
|
||||
I manage a suite of self-hosted services. A playground for
|
||||
breaking things in private. I use{" "}
|
||||
<strong>Tailscale and Woodpecker CI</strong> to orchestrate
|
||||
everything from <strong>Grafana surf dashboards</strong> to
|
||||
personal wikis.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Zap size={14} className="text-orange-600" />
|
||||
<p className="font-bold text-slate-900 text-xs">
|
||||
Product Prototyping
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-[12px] print:text-[10px] text-slate-600 leading-snug">
|
||||
Building quirky apps like a "not-pokemon" pet
|
||||
collecting game and a space-rocket countdown dashboard.
|
||||
Check out my <strong>portfolio website</strong> for full
|
||||
details!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xs uppercase tracking-[0.2em] font-black text-slate-400 mb-6 print:mb-3 border-b border-slate-100 pb-2 text-slate-900">
|
||||
Human
|
||||
</h2>
|
||||
<p className="text-[12px] print:text-[10px] text-slate-600 leading-relaxed italic">
|
||||
Surfing, photography, and following space news. Usually found
|
||||
watching anime while tinkering with my server stack. Currently
|
||||
learning Danish (undskyld på forhånd).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="pt-4 print:pt-2">
|
||||
<div className="p-4 print:p-3 bg-orange-50 rounded-sm border-l-4 border-orange-600">
|
||||
<p className="text-[10px] font-bold text-orange-600 uppercase mb-2 print:mb-1">
|
||||
Portfolio Deep Dive
|
||||
</p>
|
||||
<p className="text-[12px] print:text-[10px] text-slate-700 mb-3 print:mb-1.5 leading-tight">
|
||||
Detailed architecture diagrams and documentation:
|
||||
</p>
|
||||
<a
|
||||
href="https://georgew.dev"
|
||||
className="text-sm print:hidden font-black flex items-center gap-1 hover:underline text-slate-900"
|
||||
>
|
||||
GEORGEW.DEV <ExternalLink size={14} />
|
||||
</a>
|
||||
<div className="hidden print:block text-sm font-black text-slate-900">
|
||||
GEORGEW.DEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
|
@ -1,3 +1,5 @@
|
|||
export const dynamic = "force-dynamic";
|
||||
|
||||
import ForgeUI from "@/components/ForgeUI";
|
||||
import PageLayout from "@/components/PageLayout";
|
||||
import { getAllForgeProjects } from "@/lib/git";
|
||||
|
|
|
|||
|
|
@ -31,3 +31,62 @@ body {
|
|||
max-width: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0 !important; /* Use 0 here, and handle padding in the React component */
|
||||
-webkit-margin-before: 0;
|
||||
-webkit-margin-after: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Force the body to be the exact width of A4 to prevent scaling */
|
||||
body {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: white !important;
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
}
|
||||
|
||||
/* Hide everything outside the CV content */
|
||||
#safari-extension-is-installed,
|
||||
main a[href="/"],
|
||||
main a[href*="back"],
|
||||
footer,
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Ensure the PageLayout wrapper doesn't add width or centering */
|
||||
main {
|
||||
display: block !important;
|
||||
min-height: auto !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
/* 3. Ensure the inner container doesn't push content down */
|
||||
main > div {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* 4. Fix the "page break" issue after the title */
|
||||
h1,
|
||||
header {
|
||||
break-after: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ const geistMono = Geist_Mono({
|
|||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "George W.",
|
||||
description: "Portfolio site",
|
||||
icons: {
|
||||
icon: [{ url: "/favicon.svg", type: "image/svg+xml" }],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
|
|
|||
16
app/page.tsx
16
app/page.tsx
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Link from "next/link";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import { Globe, Smartphone, Server, Gamepad2, Hammer } from "lucide-react";
|
||||
import { Globe, Smartphone, Server, Hammer } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import MonitorCard from "@/components/MonitorCard";
|
||||
import PageLayout from "@/components/PageLayout";
|
||||
|
|
@ -47,23 +47,29 @@ export default function Home() {
|
|||
</p>
|
||||
<div className="flex gap-6 text-[10px] font-mono tracking-[0.2em] uppercase mt-4">
|
||||
<a
|
||||
href="https://git.georgew.dev"
|
||||
href="https://git.georgew.dev/georgew"
|
||||
className="text-neutral-500 hover:text-white transition-colors"
|
||||
>
|
||||
Git
|
||||
</a>
|
||||
<a
|
||||
href="https://linkedin.com/in/georgew"
|
||||
href="https://www.linkedin.com/in/george-webberley/"
|
||||
className="text-neutral-500 hover:text-white transition-colors"
|
||||
>
|
||||
LinkedIn
|
||||
</a>
|
||||
<Link
|
||||
href="/cv"
|
||||
className="text-neutral-500 hover:text-white transition-colors"
|
||||
>
|
||||
CV
|
||||
</Link>
|
||||
</div>
|
||||
</motion.header>
|
||||
|
||||
{/* Main Bento Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-6">
|
||||
{/* Top Row Left: The Architect */}
|
||||
{/* Top Row Left: Technical Focus */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="md:col-span-4 p-8 rounded-3xl bg-neutral-900/50 border border-neutral-800 flex flex-col md:flex-row gap-8 min-h-[300px] overflow-hidden relative"
|
||||
|
|
@ -71,7 +77,7 @@ export default function Home() {
|
|||
<div className="flex-[1.5] flex flex-col justify-between relative z-10">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-4 tracking-tight">
|
||||
The Architect
|
||||
Technical Focus
|
||||
</h2>
|
||||
<p className="text-base text-neutral-400 leading-relaxed max-w-lg">
|
||||
Bridging the gap between rigid regulatory requirements and
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@ import Image from "next/image";
|
|||
|
||||
const MONITORS = [
|
||||
{ id: 2, name: "Datasaur" },
|
||||
{ id: 12, name: "Observatory" },
|
||||
{ id: 16, name: "Fossil tracker" },
|
||||
{ id: 6, name: "Audiobookshelf" },
|
||||
{ id: 7, name: "Woodpecker CI" },
|
||||
{ id: 8, name: "Forgejo Git" },
|
||||
{ id: 9, name: "Server dashboard" },
|
||||
{ id: 10, name: "Ratoong" },
|
||||
{ id: 3, name: "Dozzle" },
|
||||
{ id: 12, name: "Observatory" },
|
||||
{ id: 13, name: "Surf hub" },
|
||||
{ id: 11, name: "Anime list" },
|
||||
{ id: 5, name: "Wiki" },
|
||||
{ id: 4, name: "Watchtower" },
|
||||
{ id: 14, name: "Paperless" },
|
||||
];
|
||||
|
||||
const ITEMS_PER_PAGE = 6;
|
||||
|
|
|
|||
12
data/lab.ts
12
data/lab.ts
|
|
@ -13,6 +13,18 @@ export const LAB_SERVICES: LabService[] = [
|
|||
image: "/lab/observatory.jpg",
|
||||
uptimeId: 12,
|
||||
},
|
||||
{
|
||||
id: "dino-tracker",
|
||||
name: "GW Paleo",
|
||||
description:
|
||||
"A digital field journal and taxonomic registry for Dinosauria. Designed with a 'Museum Archive' aesthetic, this specimen tracker syncs with the Paleobiology Database (PBDB) to catalog thousands of validated species and genera.",
|
||||
stack: ["Next.js", "Prisma", "SQLite", "Tailwind CSS"],
|
||||
visibility: "public",
|
||||
url: "https://paleo.georgew.dev",
|
||||
gitUrl: "https://git.georgew.dev/georgew/dino-tracker",
|
||||
image: "/lab/dino-tracker.jpg",
|
||||
uptimeId: 16,
|
||||
},
|
||||
{
|
||||
id: "surf-hub",
|
||||
name: "Surf Sentinel",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ A key architectural pillar was the implementation of a robust **Security Rules**
|
|||
"/projects/ratoong/ratoong-4.jpg",
|
||||
"/projects/ratoong/ratoong-5.jpg",
|
||||
],
|
||||
liveUrl: "https://ratoong.com",
|
||||
liveUrl: "https://www.ratoong.com/",
|
||||
isPrivate: false,
|
||||
mermaidChart: `
|
||||
graph LR
|
||||
|
|
@ -104,7 +104,7 @@ graph LR
|
|||
"/projects/datasaur/datasaur-6.jpg",
|
||||
],
|
||||
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||
liveUrl: "https://datasaur.georgew.dev", // Adjusted based on your self-hosting mention
|
||||
liveUrl: "https://datasaur.dev",
|
||||
isPrivate: false,
|
||||
engineeringStory: `
|
||||
Datasaur was born out of a necessity to bridge the gap between raw survey data and academic-grade statistical insights. The challenge wasn't just displaying data, but architecting a system capable of performing complex mathematical computations on-the-fly.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ services:
|
|||
- NODE_ENV=production
|
||||
networks:
|
||||
- web_traffic
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
networks:
|
||||
web_traffic:
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import type { NextConfig } from "next";
|
|||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
// assetPrefix:
|
||||
// process.env.NODE_ENV === "production"
|
||||
// ? "https://cdn.georgew.dev"
|
||||
// : undefined,
|
||||
// images: {
|
||||
// loader: "imgix",
|
||||
// path: "https://cdn.georgew.dev/",
|
||||
// },
|
||||
assetPrefix:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "https://georgew.b-cdn.net"
|
||||
: undefined,
|
||||
images: {
|
||||
domains: ["georgew.b-cdn.net"],
|
||||
loader: "default",
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
5
public/.well-known/nostr.json
Normal file
5
public/.well-known/nostr.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"names": {
|
||||
"muninn": "44e345442475c960433feb762c9d3f70e4fdb71c2f873a3473358d40e2ae01c1"
|
||||
}
|
||||
}
|
||||
16
public/favicon.svg
Normal file
16
public/favicon.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#0a0a0a"/>
|
||||
<text
|
||||
x="50%"
|
||||
y="52%"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="middle"
|
||||
fill="#f97316"
|
||||
font-family="system-ui, sans-serif"
|
||||
font-weight="900"
|
||||
font-size="22"
|
||||
style="transform-origin: center; transform: scale(1, 1.2);"
|
||||
>
|
||||
<>
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 409 B |
BIN
public/lab/dino-tracker.jpg
Normal file
BIN
public/lab/dino-tracker.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
BIN
public/profile.jpg
Normal file
BIN
public/profile.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Loading…
Reference in a new issue