Compare commits

..

No commits in common. "main" and "v1.0.6" have entirely different histories.
main ... v1.0.6

9 changed files with 29 additions and 503 deletions

View file

@ -1,47 +1,36 @@
# Portfolio 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).
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. ## Getting Started
## 🏗️ Architecture Summary First, run the development server:
- **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 ```bash
docker buildx build --platform linux/amd64 -t portfolio:latest . npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
``` ```
### Automated Pipeline Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Deployments are triggered via Git Releases. The Woodpecker pipeline executes the following steps: You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-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.
## 🧪 The_Forge Integration 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" is a dynamic project log system that provides a glimpse into active R&D cycles. ## Learn More
-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:
Built for performance and technical transparency. - [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.

View file

@ -1,381 +0,0 @@
"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. Im 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&apos;m always looking for the most
efficient way to get a project from &apos;concept&apos; to
&apos;shipped&apos;.
</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 &quot;not-pokemon&quot; 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 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>
);
}

View file

@ -31,62 +31,3 @@ body {
max-width: none; max-width: none;
word-break: break-word; 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;
}
}

View file

@ -2,7 +2,7 @@
import Link from "next/link"; import Link from "next/link";
import { motion, Variants } from "framer-motion"; import { motion, Variants } from "framer-motion";
import { Globe, Smartphone, Server, Hammer } from "lucide-react"; import { Globe, Smartphone, Server, Gamepad2, Hammer } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import MonitorCard from "@/components/MonitorCard"; import MonitorCard from "@/components/MonitorCard";
import PageLayout from "@/components/PageLayout"; import PageLayout from "@/components/PageLayout";
@ -58,12 +58,6 @@ export default function Home() {
> >
LinkedIn LinkedIn
</a> </a>
<Link
href="/cv"
className="text-neutral-500 hover:text-white transition-colors"
>
CV
</Link>
</div> </div>
</motion.header> </motion.header>

View file

@ -7,17 +7,17 @@ import Image from "next/image";
const MONITORS = [ const MONITORS = [
{ id: 2, name: "Datasaur" }, { id: 2, name: "Datasaur" },
{ id: 12, name: "Observatory" },
{ id: 16, name: "Fossil tracker" },
{ id: 6, name: "Audiobookshelf" }, { id: 6, name: "Audiobookshelf" },
{ id: 7, name: "Woodpecker CI" }, { id: 7, name: "Woodpecker CI" },
{ id: 8, name: "Forgejo Git" }, { id: 8, name: "Forgejo Git" },
{ id: 9, name: "Server dashboard" }, { id: 9, name: "Server dashboard" },
{ id: 10, name: "Ratoong" },
{ id: 3, name: "Dozzle" }, { id: 3, name: "Dozzle" },
{ id: 12, name: "Observatory" },
{ id: 13, name: "Surf hub" }, { id: 13, name: "Surf hub" },
{ id: 11, name: "Anime list" }, { id: 11, name: "Anime list" },
{ id: 5, name: "Wiki" }, { id: 5, name: "Wiki" },
{ id: 14, name: "Paperless" }, { id: 4, name: "Watchtower" },
]; ];
const ITEMS_PER_PAGE = 6; const ITEMS_PER_PAGE = 6;

View file

@ -13,18 +13,6 @@ export const LAB_SERVICES: LabService[] = [
image: "/lab/observatory.jpg", image: "/lab/observatory.jpg",
uptimeId: 12, 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", id: "surf-hub",
name: "Surf Sentinel", name: "Surf Sentinel",

View file

@ -1,5 +0,0 @@
{
"names": {
"muninn": "44e345442475c960433feb762c9d3f70e4fdb71c2f873a3473358d40e2ae01c1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB