Added more project details
Some checks are pending
ci/woodpecker/release/woodpecker Pipeline is running

This commit is contained in:
GeorgeWebberley 2026-01-30 21:01:59 +01:00
parent bd038c4b0d
commit a09509581f
17 changed files with 400 additions and 51 deletions

View file

@ -226,7 +226,7 @@ export default function Home() {
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<p>Pipeline Status</p>
<Image
<img
src="https://ci.georgew.dev/api/badges/11/status.svg"
alt="Build Status"
className="h-3 grayscale opacity-50 hover:opacity-100 hover:grayscale-0 transition-all"

View file

@ -0,0 +1,110 @@
"use client";
import { useEffect, useRef, useState } from "react";
import mermaid from "mermaid";
import { motion, AnimatePresence } from "framer-motion";
import { Maximize2, Minimize2 } from "lucide-react";
export default function Mermaid({ chart }: { chart: string }) {
const [isExpanded, setIsExpanded] = useState(false);
const [isRendered, setIsRendered] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: false,
theme: "dark",
securityLevel: "loose",
fontFamily: "monospace",
});
// Render the chart once. Use a timeout to ensure DOM is stable.
const renderChart = async () => {
try {
await mermaid.contentLoaded();
setIsRendered(true);
} catch (err) {
console.error("Mermaid render failed:", err);
}
};
renderChart();
}, [chart]); // Only re-run if the chart string itself changes
return (
<div className="relative max-w-4xl mx-auto group">
<motion.div
layout
initial={false}
animate={{
height: isExpanded ? "auto" : "400px",
}}
transition={{ duration: 0.6, ease: [0.23, 1, 0.32, 1] }}
className={`relative bg-neutral-900/20 rounded-3xl border border-neutral-800/50 overflow-hidden transition-colors duration-500 ${
!isExpanded
? "hover:border-neutral-700 cursor-pointer"
: "cursor-default"
}`}
onClick={() => !isExpanded && setIsExpanded(true)}
>
{/* Legend */}
<div className="absolute top-6 right-8 flex flex-col gap-2 z-20 bg-black/40 backdrop-blur-md p-3 rounded-xl border border-white/5 pointer-events-none">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<span className="text-[9px] font-mono text-neutral-400 uppercase tracking-wider">
Traffic Flow
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span className="text-[9px] font-mono text-neutral-400 uppercase tracking-wider">
Service Node
</span>
</div>
</div>
{/* Chart Area */}
<div
className={`p-4 md:p-12 transition-opacity duration-500 ${isRendered ? "opacity-100" : "opacity-0"}`}
>
<div className="mermaid flex justify-center">{chart}</div>
</div>
{/* Fade Overlay */}
<AnimatePresence>
{!isExpanded && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/90 to-transparent pointer-events-none"
/>
)}
</AnimatePresence>
</motion.div>
{/* Toggle Button */}
<button
onClick={(e) => {
e.stopPropagation();
setIsExpanded(!isExpanded);
}}
className={`absolute -bottom-5 left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 px-5 py-2.5 rounded-full text-[10px] font-mono uppercase tracking-[0.2em] transition-all border shadow-xl
${
isExpanded
? "bg-neutral-800 border-neutral-700 text-white"
: "bg-neutral-900 border-neutral-800 text-neutral-500 group-hover:bg-neutral-800 group-hover:border-neutral-600 group-hover:text-white"
}`}
>
{isExpanded ? (
<>
<Minimize2 size={12} /> Collapse Logic
</>
) : (
<>
<Maximize2 size={12} /> Expand Architecture
</>
)}
</button>
</div>
);
}

View file

@ -0,0 +1,98 @@
"use client";
import { useEffect, useState, useRef } from "react";
import mermaid from "mermaid";
import { motion, AnimatePresence } from "framer-motion";
import { Maximize2, Minimize2 } from "lucide-react";
export default function Mermaid({ chart }: { chart: string }) {
const [isExpanded, setIsExpanded] = useState(false);
const [needsExpansion, setNeedsExpansion] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: true,
theme: "dark",
securityLevel: "loose",
fontFamily: "monospace",
});
mermaid.contentLoaded();
if (contentRef.current) {
const height = contentRef.current.scrollHeight;
setNeedsExpansion(height > 400);
}
}, [chart]);
return (
<div className="relative max-w-4xl mx-auto group">
<motion.div
initial={false}
onClick={() => needsExpansion && setIsExpanded(!isExpanded)}
animate={{ height: isExpanded || !needsExpansion ? "auto" : "400px" }}
className={`relative bg-neutral-900/20 rounded-3xl border border-neutral-800/50 p-4 md:p-12 overflow-hidden transition-colors duration-500
${needsExpansion && !isExpanded ? "cursor-pointer hover:border-neutral-700" : "cursor-default"}`}
>
{/* Legend */}
<div className="absolute top-6 right-8 flex flex-col gap-2 z-20 bg-black/40 backdrop-blur-md p-3 rounded-xl border border-white/5">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<span className="text-[9px] font-mono text-neutral-400 uppercase tracking-wider">
Traffic Flow
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span className="text-[9px] font-mono text-neutral-400 uppercase tracking-wider">
Service Node
</span>
</div>
</div>
<div ref={contentRef} className="mermaid flex justify-center">
{chart}
</div>
{/* The "Fade to Darkness" Overlay (when expansion is needed) */}
<AnimatePresence>
{needsExpansion && !isExpanded && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/80 to-transparent pointer-events-none"
/>
)}
</AnimatePresence>
</motion.div>
{/* Expand/Collapse Button (when expansion is needed) */}
{needsExpansion && (
<button
onClick={(e) => {
e.stopPropagation();
setIsExpanded(!isExpanded);
}}
className={`absolute -bottom-5 left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 px-5 py-2.5 rounded-full text-[10px] font-mono uppercase tracking-[0.2em] transition-all border shadow-xl
${
isExpanded
? "bg-neutral-800 border-neutral-700 text-white"
: "bg-neutral-900 border-neutral-800 text-neutral-500 group-hover:bg-neutral-800 group-hover:border-neutral-600 group-hover:text-white"
}`}
>
{isExpanded ? (
<>
{" "}
<Minimize2 size={12} /> Collapse Logic{" "}
</>
) : (
<>
{" "}
<Maximize2 size={12} /> Expand Architecture{" "}
</>
)}
</button>
)}
</div>
);
}

View file

@ -114,12 +114,12 @@ export default function MonitorRegistry({ isHovered }: { isHovered: boolean }) {
{m.name}
</span>
<div className="flex gap-1 shrink-0 scale-90 origin-right">
<Image
<img
src={`https://status.georgew.dev/api/badge/${m.id}/status`}
className="h-5"
alt="up"
/>
<Image
<img
src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`}
className="h-5 opacity-60"
alt="ms"

View file

@ -43,7 +43,7 @@ export default function ProjectShowcase({ images }: { images: string[] }) {
: "border-neutral-800 opacity-40 hover:opacity-100"
}`}
>
<Image
<img
src={img}
className="h-full w-full object-cover"
alt={`Thumb ${i}`}

View file

@ -1,17 +1,22 @@
import { Project } from "@/types/project";
export const PROJECT_REGISTRY: Project[] = [
{
slug: "ratoong",
category: "web",
title: "Ratoong",
subtitle: "High-Performance Ski & Travel Engine",
role: "Full-Stack Engineer",
duration: "2020 — 2022", // Adjusted based on your "long time ago" comment
stack: ["Angular", "Firebase", "GCP Cloud Functions", "TypeScript"],
metrics: ["< 200ms Search Latency", "10,000+ Active Data Points", "Fully Responsive Design"],
description: "A comprehensive ski resort planning and rating platform featuring real-time weather integration and complex multi-parameter search filters.",
engineeringStory: `
{
slug: "ratoong",
category: "web",
title: "Ratoong",
subtitle: "High-Performance Ski & Travel Engine",
role: "Full-Stack Engineer",
duration: "2020 — 2022",
stack: ["Angular", "Firebase", "GCP Cloud Functions", "TypeScript"],
metrics: [
"< 200ms Search Latency",
"10,000+ Active Data Points",
"Fully Responsive Design",
],
description:
"A comprehensive ski resort planning and rating platform featuring real-time weather integration and complex multi-parameter search filters.",
engineeringStory: `
Building Ratoong was an exercise in managing **High-Density Data** within a reactive frontend ecosystem. The core challenge was transforming thousands of resort data pointsranging from piste lengths to real-time weatherinto a lightning-fast, searchable interface.
#### Data Orchestration & Efficiency
@ -27,11 +32,11 @@ Working with a **Backend-as-a-Service (BaaS)** model taught me the importance of
A key architectural pillar was the implementation of a robust **Security Rules** layer within Firebase. By moving the logic from the client to the database level, we ensured that resort metadata was globally searchable while sensitive user planning data remained strictly isolated. This event-driven security model allowed us to scale the user base without increasing the risk surface area of the platform.
`,
images: [
"/projects/ratoong/ratoong-1.jpg",
"/projects/ratoong/ratoong-2.jpg",
"/projects/ratoong/ratoong-3.jpg",
"/projects/ratoong/ratoong-4.jpg",
"/projects/ratoong/ratoong-5.jpg"
"/projects/ratoong/ratoong-1.jpg",
"/projects/ratoong/ratoong-2.jpg",
"/projects/ratoong/ratoong-3.jpg",
"/projects/ratoong/ratoong-4.jpg",
"/projects/ratoong/ratoong-5.jpg",
],
liveUrl: "https://ratoong.com",
isPrivate: false,
@ -72,42 +77,174 @@ graph LR
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
classDef hub fill:#f59e0b,stroke:#d97706,color:#fff,stroke-width:2px
`,
storyLabel: "DATA // UI EFFICIENCY",
},
{
storyLabel: "DATA // UI EFFICIENCY",
},
{
slug: "datasaur",
category: "web",
title: "Datasaur",
subtitle: "Personal R&D Pipeline",
role: "Architect & Creator",
duration: "2025 — Present",
stack: ["Python", "FastAPI", "Next.js", "Redis"],
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
images: ["/datasaur-1.jpg"],
subtitle: "Automated Statistical Analysis Engine",
role: "Lead Architect & Creator",
duration: "2019 — 2021", // Reflecting "one of my first things"
stack: ["Python", "Flask", "MongoDB", "Pandas", "SciPy"],
metrics: [
"Automated Stat-Testing",
"Multi-Format ETL",
"Self-Hosted Architecture",
],
description:
"A comprehensive survey data platform that automates complex statistical workflows, from raw data aggregation to advanced hypothesis testing and visualization.",
storyLabel: "ALGORITHMIC // STATISTICAL PROCESSING",
images: [
"/projects/datasaur/datasaur-1.jpg",
"/projects/datasaur/datasaur-2.jpg",
"/projects/datasaur/datasaur-3.jpg",
"/projects/datasaur/datasaur-4.jpg",
"/projects/datasaur/datasaur-5.jpg",
"/projects/datasaur/datasaur-6.jpg",
],
repoUrl: "https://git.georgew.dev/georgew/datasaur",
liveUrl: "https://ratoong.com",
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
storyLabel: "DATA EFFICIENCY",
isPrivate: true,
liveUrl: "https://datasaur.georgew.dev", // Adjusted based on your self-hosting mention
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.
#### Statistical Automation Pipeline
The core of the application is a robust processing engine built on **Pandas** and **SciPy**. I implemented automated workflows for non-parametric tests like **Kruskal-Wallis** and **Mann-Whitney U**, ensuring that the platform could intelligently suggest and execute the correct statistical test based on the data distribution.
#### Data Visualization & Export
To translate these numbers into insights, I built a visualization layer supporting everything from standard histograms to complex **Box and Whisker** plots. Using **XlsxWriter**, I developed a custom export engine that allowed users to pull processed data directly into professional-grade spreadsheets with pre-formatted statistical summaries.
#### Infrastructure & Monolithic Integrity
The project follows a classic monolithic architecture, which proved highly efficient for keeping memory-intensive dataframes close to the processing logic. Today, the platform is self-hosted using a **Caddy** reverse proxy and **MongoDB Atlas**, demonstrating the longevity and stability of a well-architected Flask ecosystem.
`,
mermaidChart: `
graph LR
subgraph Client_Layer [User Interface]
A[Vanilla JS / Browser]:::traffic
end
subgraph Server_Layer [Application Logic]
B[Caddy Reverse Proxy]:::node
C[Flask / Python Monolith]:::node
end
subgraph Processing_Engine [Data Science Core]
D[Pandas ETL]:::node
E[SciPy / Pingouin Stats]:::node
F[XlsxWriter Export]:::node
end
subgraph Storage [Data Persistence]
G[MongoDB Atlas]:::node
end
A <-->|HTTPS| B
B <-->|WSGI| C
C <-->|Query/Write| G
C ==>|Dataframes| D
D --> E
D --> F
%% Styles %%
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
`,
},
{
slug: "ayla",
category: "web",
category: "infrastructure",
title: "Ayla",
subtitle: "Regulatory-Compliant Data Platform",
role: "Lead Full-Stack Engineer",
subtitle: "Regulatory-Compliant Medical Platform",
role: "Tech Lead & Scrum Master",
duration: "2022 — 2024",
stack: ["Node.js", "PostgreSQL", "React", "Docker"],
metrics: ["99.9% Uptime", "Zero-Data-Loss Integrity", "ISO 27001 Ready"],
description: "Architected a high-integrity platform designed to meet rigid regulatory requirements.",
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
images: ["/ratoong-hero.jpg", "/ratoong-dashboard.jpg"],
liveUrl: "https://ratoong.com",
stack: [
"Kubernetes",
"Ruby on Rails",
"Flutter",
"Terraform",
"GCP",
"OTC",
],
metrics: [
"Multi-Region Data Residency",
"ISO 27001 Compliant",
"Single-Click IaC Deployment",
],
description:
"A high-availability medical device platform supporting dementia treatment, featuring multi-cloud infrastructure, automated pipelines, cross-platform web and mobile deployments and strict regulatory requirements.",
storyLabel: "GOVERNANCE // CLOUD ORCHESTRATION",
images: [
"/projects/ayla/ayla-1.jpg",
"/projects/ayla/ayla-2.jpg",
"/projects/ayla/ayla-3.jpg",
"/projects/ayla/ayla-4.jpg",
"/projects/ayla/ayla-5.jpg",
],
isPrivate: true,
storyLabel: "DATA EFFICIENCY",
engineeringStory: `
As Tech Lead for Ayla, I was responsible for architecting a platform that met the rigorous safety and security standards of a certified medical device. This required a "Security-by-Design" approach, balancing high availability (SLA) with rigid data residency requirements across the UK and EU.
#### Multi-Cloud Infrastructure & IaC
To satisfy GDPR and local health data regulations, I architected a dual-cloud strategy: **Open Telekom Cloud (OTC)** for European users and **GCP** for the UK. Using **Terraform**, I codified the entire infrastructure, enabling us to spin up identical, audit-ready Kubernetes clusters or Cloud Run environments in minutes. This automation was critical for maintaining the "Release-Pre-Release" protocols required for medical certification.
#### Full-Stack Delivery & Automation
The platform featured a **Flutter** frontend for Web, iOS, and Android, all managed through automated **CICD** pipelines. I implemented a layered automation strategy, combining **GitHub Actions** for web deployments and server-side logic with **Fastlane** for mobile app store distribution. The backend was a high-performance **Ruby on Rails** API, architected as a stateless "mini-service" to ensure horizontal scalability within Kubernetes. I also integrated **Squidex CMS** to empower non-technical colleagues to manage content without compromising the system's core integrity.
#### Leadership & Compliance
Beyond the code, I served as Scrum Master and Product Owner, leading sprint planning, retro and demos. I worked closely with regulatory partners and personally oversaw the creation of **DPIAs**, **Cyber Essentials** certification, and the path to **ISO 27001** compliance. In the absence of a dedicated IT department, I managed the MDM systems and sysadmin duties, ensuring that every layer of the organization met the strict regulatory bar.
`,
mermaidChart: `
graph TB
%% Direction and Layout
direction TB
subgraph Shared_Ops [DevOps & CMS]
I[GitHub Actions CICD]:::traffic
J[Terraform IaC]:::traffic
K[Squidex CMS]:::node
end
subgraph Frontend_Layer [Omni-Channel]
A[Flutter Web / Mobile]:::traffic
B[Bunny CDN / Edge Storage]:::node
end
subgraph UK_Region [GCP]
G[Cloud Run Containers]:::node
H[Cloud SQL]:::node
end
subgraph EU_Region [Open Telekom Cloud]
D[NGINX Ingress]:::node
C[K8s Cluster]:::node
F[Object Storage]:::node
E[PostgreSQL RDS]:::node
end
%% Connections
A <--> B
I -->|Fastlane| A
J -->|Provision| G
J -->|Provision| C
B <-->|UK Traffic| G
B <-->|EU Traffic| D
D --> C
G <--> H
C <--> E
C --- F
G <--> K
C <--> K
%% Styles
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
`,
},
{
{
slug: "flutter-1",
category: "mobile",
title: "Flutter-1",
@ -116,15 +253,17 @@ graph LR
duration: "2025 — Present",
stack: ["Python", "FastAPI", "Next.js", "Redis"],
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
description:
"A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
images: ["/datasaur-1.jpg"],
repoUrl: "https://git.georgew.dev/georgew/datasaur",
liveUrl: "https://ratoong.com",
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
engineeringStory:
"In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
storyLabel: "DATA EFFICIENCY",
isPrivate: true,
},
{
{
slug: "flutter-2",
category: "mobile",
title: "Flutter-1",
@ -133,12 +272,14 @@ graph LR
duration: "2025 — Present",
stack: ["Python", "FastAPI", "Next.js", "Redis"],
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
description:
"A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
images: ["/datasaur-1.jpg"],
repoUrl: "https://git.georgew.dev/georgew/datasaur",
liveUrl: "https://ratoong.com",
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
engineeringStory:
"In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
storyLabel: "DATA EFFICIENCY",
isPrivate: true,
},
];
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB