Compare commits

...

6 commits

Author SHA1 Message Date
GeorgeWebberley a09509581f Added more project details
Some checks are pending
ci/woodpecker/release/woodpecker Pipeline is running
2026-01-30 21:01:59 +01:00
GeorgeWebberley bd038c4b0d Cleaned up warnings 2026-01-30 12:42:41 +01:00
GeorgeWebberley 1ff0d61a19 Added prettier auto-format and cleaned up comments 2026-01-30 12:40:25 +01:00
GeorgeWebberley 49e62d5e2f Fixed tailwind markdown beahviour and improved aesthetics on the project details page 2026-01-30 12:31:47 +01:00
GeorgeWebberley 471b251fd7 Commiting changes before upgrade 2026-01-30 11:22:44 +01:00
GeorgeWebberley 5d0a86645d Added project details pages. Cleaned up dashboard 2026-01-29 20:54:37 +01:00
35 changed files with 4347 additions and 747 deletions

0
.prettierrc Normal file
View file

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"css.lint.unknownAtRules": "ignore",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

View file

@ -1,4 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@tailwindcss/typography";
:root { :root {
--background: #ffffff; --background: #ffffff;
@ -24,3 +25,9 @@ body {
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
.prose {
width: 100%;
max-width: none;
word-break: break-word;
}

View file

@ -1,128 +0,0 @@
"use client";
import Link from 'next/link';
import { motion } from 'framer-motion';
import { Globe, Smartphone, Server, Gamepad2, Activity } from 'lucide-react';
export default function Home() {
return (
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
<header className="mb-12">
<h1 className="text-4xl font-bold tracking-tight">George W.</h1>
<p className="text-neutral-400 mt-2">Senior Full Stack Engineer & Tech Lead</p>
</header>
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Top Row: The Architect (Wide) */}
<div className="md:col-span-2 p-8 rounded-3xl bg-neutral-900/50 border border-neutral-800 flex flex-col justify-between min-h-[320px]">
<div>
<h2 className="text-3xl font-bold mb-4">The Architect</h2>
<p className="text-neutral-400 leading-relaxed max-w-2xl">
Bridging the gap between complex system architecture and fluid user experiences.
I specialize in designing distributed web systems and cross-platform mobile apps
with a relentless focus on performance, self-sovereign infrastructure, and automated delivery pipelines.
</p>
</div>
<div className="flex flex-wrap gap-3 mt-8 text-[10px] font-mono text-neutral-600">
{['#Architecture', '#SystemDesign', '#Automation', '#FullStack', '#Scalability'].map(tag => (
<span key={tag}>{tag}</span>
))}
</div>
</div>
{/* Top Row: Hetzner Node (Narrow) */}
<div className="p-8 rounded-3xl bg-neutral-900/50 border border-neutral-800 flex flex-col justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-sm font-medium">Hetzner Node-01</span>
</div>
<div className="flex justify-between items-end">
<span className="text-xs text-neutral-500 uppercase tracking-widest">Status: UP</span>
<Activity className="text-neutral-800 w-12 h-12" />
</div>
</div>
{/* Middle Row: Web Systems */}
<Link href="/projects/web" className="group">
<motion.div
whileHover={{ y: -5 }}
className="p-6 rounded-3xl bg-neutral-900 border border-neutral-800 h-full flex flex-col justify-between min-h-[260px] relative overflow-hidden transition-colors hover:border-blue-500/30"
>
<div>
<Globe className="text-blue-400 w-6 h-6 mb-4" />
<h3 className="font-bold text-xl mb-2">Web Systems</h3>
<p className="text-sm text-neutral-500 leading-relaxed">
Architecting distributed platforms with a focus on high-availability, API design, and containerized deployment.
</p>
</div>
<div className="flex flex-wrap gap-2 mt-6">
{['Next.js', 'Python', 'Node.js', 'Caddy', 'PostgreSQL'].map(tech => (
<span key={tech} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-blue-400 group-hover:border-blue-500/20">
{tech}
</span>
))}
</div>
</motion.div>
</Link>
{/* Middle Row: Mobile Apps */}
<Link href="/projects/mobile" className="group">
<motion.div
whileHover={{ y: -5 }}
className="p-6 rounded-3xl bg-neutral-900 border border-neutral-800 h-full flex flex-col justify-between min-h-[260px] relative overflow-hidden transition-colors hover:border-purple-500/30"
>
<div>
<Smartphone className="text-purple-400 w-6 h-6 mb-4" />
<h3 className="font-bold text-xl mb-2">Mobile Apps</h3>
<p className="text-sm text-neutral-500 leading-relaxed">
Building fluid, cross-platform experiences using reactive state management and native hardware integration.
</p>
</div>
<div className="flex flex-wrap gap-2 mt-6">
{['Android', 'iOS', 'Flutter', 'Riverpod', 'Stores'].map(tech => (
<span key={tech} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-purple-400 group-hover:border-purple-500/20">
{tech}
</span>
))}
</div>
</motion.div>
</Link>
{/* Middle Row: Infrastructure (The New Card) */}
<Link href="/projects/infrastructure" className="group">
<motion.div
whileHover={{ y: -5 }}
className="p-6 rounded-3xl bg-neutral-900 border border-neutral-800 h-full flex flex-col justify-between min-h-[260px] relative overflow-hidden transition-colors hover:border-green-500/30"
>
<div>
<Server className="text-green-400 w-6 h-6 mb-4" />
<h3 className="font-bold text-xl mb-2">DevOps</h3>
<p className="text-sm text-neutral-500 leading-relaxed">
Managing self-hosted cloud nodes with automated CI/CD pipelines, secure proxying, and proactive monitoring.
</p>
</div>
<div className="flex flex-wrap gap-2 mt-6">
{['Docker', 'Woodpecker', 'Hetzner', 'Linux', 'Uptime'].map(tech => (
<span key={tech} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-green-400 group-hover:border-green-500/20">
{tech}
</span>
))}
</div>
</motion.div>
</Link>
{/* Bottom Row: The Forge (Wide) */}
<div className="md:col-span-3 p-8 rounded-3xl bg-neutral-900/50 border border-neutral-800 flex items-center gap-6">
<div className="p-4 bg-orange-500/10 rounded-2xl border border-orange-500/20">
<Gamepad2 className="text-orange-500 w-8 h-8" />
</div>
<div>
<h3 className="font-bold text-xl">The Forge</h3>
<p className="text-sm text-neutral-500">Indie Game Dev & Creative Prototypes</p>
</div>
</div>
</div>
</main>
);
}

View file

@ -1,195 +0,0 @@
"use client";
import { motion } from "framer-motion";
import { Server, Globe, Smartphone, Gamepad2, Activity } from "lucide-react";
import Link from 'next/link';
export default function Home() {
return (
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
{/* Header section */}
<header className="mb-12">
<h1 className="text-4xl font-bold tracking-tight">George W.</h1>
<p className="text-neutral-400 mt-2">Senior Full Stack Engineer & Tech Lead</p>
</header>
{/* Bento Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 auto-rows-[180px]">
{/* About Me - Large Card */}
<motion.div
whileHover={{ y: -5 }}
className="md:col-span-2 md:row-span-2 p-8 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-between"
>
<div>
<h2 className="text-2xl font-semibold mb-4">The Architect</h2>
<p className="text-neutral-400 leading-relaxed">
Bridging the gap between complex system architecture and fluid user experiences. I specialize in designing distributed web systems and cross-platform mobile apps with a relentless focus on performance, self-sovereign infrastructure, and automated delivery pipelines.
</p>
</div>
<div className="flex gap-2 text-xs font-mono text-neutral-500">
<span>#NextJS</span> <span>#Flutter</span> <span>#Typescript</span> <span>#Python</span> <span>#Node</span> <span>#Docker</span> <span>#Kubernetes</span> <span>#Serverless</span> <span>#CI/CD</span>
</div>
</motion.div>
{/* Live Pulse Card */}
<motion.div
whileHover={{ y: -5 }}
className="group md:col-span-2 p-6 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-center relative overflow-hidden transition-all duration-300 min-h-[180px]"
>
{/* The Monitor Map (Easily editable) */}
{(() => {
const monitors = [
{ id: 2, name: "Datasaur" },
{ id: 6, name: "Audiobookshelf" },
{ id: 7, name: "Woodpecker CI" },
{ id: 8, name: "Forgejo Git" },
{ id: 9, name: "Server dashboard" },
{ id: 10, name: "Ratoong" },
];
return (
<>
{/* Default View */}
<div className="flex items-center justify-between w-full group-hover:opacity-0 group-hover:pointer-events-none transition-opacity duration-300">
<div>
<div className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="font-medium text-white">Hetzner Node-01</span>
</div>
<div className="flex gap-2 items-center">
<p className="text-sm text-neutral-500">System Status:</p>
<img
src="https://status.georgew.dev/api/status-page/dashboard/badge"
alt="Overall Status"
className="h-5"
/>
</div>
</div>
<Activity className="text-neutral-700 w-8 h-8" />
</div>
{/* Hover View: Friendly Names */}
<div className="absolute inset-0 p-6 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-center bg-neutral-900/95 backdrop-blur-sm">
<h4 className="text-[10px] font-mono text-neutral-500 mb-3 uppercase tracking-[0.2em]">Service Registry</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{monitors.map((m) => (
<div key={m.id} className="flex items-center justify-between bg-neutral-800/40 p-2 rounded-lg border border-neutral-700/30">
<span className="text-[11px] font-medium text-neutral-300 truncate mr-2">{m.name}</span>
<div className="flex gap-1 shrink-0">
<img src={`https://status.georgew.dev/api/badge/${m.id}/status`} className="h-3" alt="up" />
<img src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`} className="h-3 opacity-60" alt="ms" />
</div>
</div>
))}
</div>
</div>
</>
);
})()}
</motion.div>
{/* Project One */}
<Link href="/projects/web" className="h-full">
<motion.div
whileHover={{ y: -5 }}
className="group p-6 rounded-3xl bg-neutral-900 border border-neutral-800 h-full relative overflow-hidden cursor-pointer flex flex-col min-h-[260px]"
>
{/* Icon Container - Fixed size ensures it never disappears */}
<div className="mb-4">
<Globe className="text-blue-400 w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="font-bold text-xl mb-1 text-white">Web Systems</h3>
<p className="text-sm text-neutral-500 leading-relaxed mb-4">
Architecting distributed platforms with a focus on high-availability, API design, and containerized deployment.
</p>
</div>
{/* Tech Pips - Use 'flex-wrap' and 'gap-y' to handle multiline safely */}
<div className="flex flex-wrap gap-x-2 gap-y-2 pt-2 border-t border-neutral-800/50">
{['Next.js', 'Node.js', 'Python', 'PostgreSQL', 'Docker'].map((tech) => (
<span key={tech} className="text-[9px] font-mono text-neutral-500 border border-neutral-800 px-2 py-1 rounded-md uppercase tracking-wider group-hover:border-blue-500/30 group-hover:text-blue-400 transition-colors">
{tech}
</span>
))}
</div>
{/* Hover Indicator */}
<div className="absolute top-6 right-6 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
</div>
</motion.div>
</Link>
{/* Project Two */}
<Link href="/projects/mobile">
<motion.div
whileHover={{ y: -5 }}
className="group p-6 rounded-3xl bg-neutral-900 border border-neutral-800 h-full relative overflow-hidden cursor-pointer flex flex-col"
>
<div className="mb-4">
<Smartphone className="text-purple-400 w-6 h-6" />
</div>
<h3 className="font-bold text-xl mb-1 text-white">Mobile Apps</h3>
<p className="text-sm text-neutral-500 leading-relaxed mb-4">
Building fluid, cross-platform experiences using reactive state management and native hardware integration.
</p>
{/* Tech Pips */}
<div className="flex flex-wrap gap-x-2 gap-y-2 pt-2 border-t border-neutral-800/50">
{['Android', 'iOS', 'Flutter', 'Riverpod', 'Publishing'].map((tech) => (
<span key={tech} className="text-[9px] font-mono text-neutral-500 border border-neutral-800 px-2 py-1 rounded-md uppercase tracking-wider group-hover:border-blue-500/30 group-hover:text-blue-400 transition-colors">
{tech}
</span>
))}
</div>
<div className="absolute top-6 right-6 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="w-2 h-2 rounded-full bg-purple-500 animate-pulse" />
</div>
</motion.div>
</Link>
{/* Game Teaser / The Lab */}
<motion.div
whileHover={{ scale: 1.02 }}
className="md:col-span-2 p-6 rounded-3xl bg-gradient-to-br from-[#111] to-[#1a1a1a] border border-neutral-800 flex items-center gap-6"
>
<div className="p-4 rounded-2xl bg-neutral-800">
<Gamepad2 className="w-8 h-8 text-orange-400" />
</div>
<div>
<h3 className="font-semibold italic">The Forge</h3>
<p className="text-sm text-neutral-500">Indie Game Dev & Prototypes</p>
</div>
</motion.div>
</div>
{/* Deployment Footer */}
<footer className="mt-12 pt-8 border-t border-neutral-900 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] text-neutral-600 font-mono tracking-wider uppercase">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<p>Pipeline Status</p>
<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"
/>
</div>
<div className="hidden md:block w-[1px] h-3 bg-neutral-800" />
<p>Engine: Next.js 15 (Standalone)</p>
</div>
<div className="flex items-center gap-4 bg-neutral-900/50 px-3 py-1 rounded-full border border-neutral-800/50">
<div className="w-1 h-1 rounded-full bg-blue-500" />
<p className="text-neutral-400">
Deploy: {process.env.NEXT_PUBLIC_APP_VERSION || 'v1.0.0-dev'}
</p>
</div>
</footer>
</main>
);
}

View file

@ -1,147 +1,126 @@
"use client"; "use client";
import Link from 'next/link'; import Link from "next/link";
import { motion } from 'framer-motion'; import { motion } from "framer-motion";
import { Globe, Smartphone, Server, Gamepad2, Activity } from 'lucide-react'; import { Globe, Smartphone, Server, Gamepad2 } from "lucide-react";
import { useState } from "react";
import MonitorCard from "@/components/MonitorCard";
import Image from "next/image";
export default function Home() { export default function Home() {
const [isHoveringMonitors, setIsHoveringMonitors] = useState(false);
return ( return (
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24"> <main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<header className="mb-12"> <header className="mb-12">
<h1 className="text-4xl font-bold tracking-tight">George W.</h1> <h1 className="text-4xl font-bold tracking-tight">George W.</h1>
<p className="text-neutral-400 mt-2">Senior Full Stack Engineer & Tech Lead</p> <p className="text-neutral-400 mt-2">
Senior Full Stack Engineer & Tech Lead
</p>
<div className="flex gap-6 text-[10px] font-mono tracking-[0.2em] uppercase mt-4"> <div className="flex gap-6 text-[10px] font-mono tracking-[0.2em] uppercase mt-4">
<a href="https://git.georgew.dev" className="text-neutral-500 hover:text-white transition-colors"> <a
href="https://git.georgew.dev"
className="text-neutral-500 hover:text-white transition-colors"
>
Git Git
</a> </a>
<a href="https://linkedin.com/in/georgew" className="text-neutral-500 hover:text-white transition-colors"> <a
href="https://linkedin.com/in/georgew"
className="text-neutral-500 hover:text-white transition-colors"
>
LinkedIn LinkedIn
</a> </a>
{/* <a href="https://github.com/georgew" className="text-neutral-500 hover:text-white transition-colors">
GitHub
</a> */}
</div> </div>
</header> </header>
<div className="grid grid-cols-1 md:grid-cols-6 gap-6"> <div className="grid grid-cols-1 md:grid-cols-6 gap-6">
{/* Top Row Left: The Architect */} {/* Top Row Left: The Architect */}
<div 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"> <div 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">
{/* Background Decoration: Subtle Grid or Blueprint */} <div className="absolute top-0 right-0 w-1/2 h-full opacity-[0.03] pointer-events-none"></div>
<div className="absolute top-0 right-0 w-1/2 h-full opacity-[0.03] pointer-events-none">
{/* You could place a subtle SVG circuit or architectural grid here */}
</div>
{/* Left: Bio & Tags */} {/* Description and tags */}
<div className="flex-[1.5] flex flex-col justify-between relative z-10"> <div className="flex-[1.5] flex flex-col justify-between relative z-10">
<div> <div>
<h2 className="text-3xl font-bold mb-4 tracking-tight">The Architect</h2> <h2 className="text-3xl font-bold mb-4 tracking-tight">
<p className="text-base text-neutral-400 leading-relaxed max-w-lg"> The Architect
Bridging the gap between rigid regulatory requirements and fluid user experiences. </h2>
I specialize in designing <span className="text-white">distributed systems</span> and <p className="text-base text-neutral-400 leading-relaxed max-w-lg">
<span className="text-white"> cross-platform mobile apps</span> with a focus on Bridging the gap between rigid regulatory requirements and
automated delivery and high-integrity code. fluid user experiences. I specialize in designing{" "}
</p> <span className="text-white">distributed systems</span> and
<span className="text-white">
{" "}
cross-platform mobile apps
</span>{" "}
with a focus on automated delivery and high-integrity code.
</p>
</div>
<div className="flex flex-wrap gap-2 mt-8">
{[
"#Architecture",
"#Regulatory Compliance",
"#Agile Leadership",
"#DevOps",
].map((tag) => (
<span
key={tag}
className="text-[10px] font-mono text-neutral-500 border border-neutral-800 px-2 py-1 rounded"
>
{tag}
</span>
))}
</div>
</div> </div>
<div className="flex flex-wrap gap-2 mt-8"> <div className="hidden md:block w-px bg-neutral-800/50 self-stretch" />
{['#Architecture', '#Regulatory Compliance', '#Agile Leadership', '#DevOps'].map(tag => (
<span key={tag} className="text-[10px] font-mono text-neutral-500 border border-neutral-800 px-2 py-1 rounded"> {/* Technical details */}
{tag} <div className="flex-1 flex flex-col justify-around py-2 relative z-10">
</span> <div className="space-y-6">
))} <section>
<h4 className="text-[12px] font-mono text-blue-500 uppercase tracking-[0.2em] mb-2">
Leadership
</h4>
<p className="text-xs text-neutral-300 leading-tight">
Tech Lead & Scrum Master. Orchestrating sprint cycles,
system design, and cross-functional team growth.
</p>
</section>
<section>
<h4 className="text-[12px] font-mono text-purple-500 uppercase tracking-[0.2em] mb-2">
Integrity
</h4>
<p className="text-xs text-neutral-300 leading-tight">
Experienced in{" "}
<span className="italic">High-Stakes Environments</span>{" "}
(Medical/Regulatory), QMS, and Cyber Essentials.
</p>
</section>
<section>
<h4 className="text-[12px] font-mono text-green-500 uppercase tracking-[0.2em] mb-2">
Infrastructure
</h4>
<p className="text-xs text-neutral-300 leading-tight">
Kubernetes, GCP, and automated CI/CD pipelines.
</p>
</section>
</div>
</div> </div>
</div> </div>
{/* Vertical Divider */} {/* Top Row Right: The Service Registry */}
<div className="hidden md:block w-px bg-neutral-800/50 self-stretch" /> <motion.div
whileHover={{ y: -5 }}
{/* Right: Seniority Specs */} onMouseEnter={() => setIsHoveringMonitors(true)}
<div className="flex-1 flex flex-col justify-around py-2 relative z-10"> onMouseLeave={() => setIsHoveringMonitors(false)}
<div className="space-y-6"> className="group md:col-span-2 p-6 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-center relative overflow-hidden transition-all duration-300 min-h-[180px]"
<section> >
<h4 className="text-[12px] font-mono text-blue-500 uppercase tracking-[0.2em] mb-2">Leadership</h4> <MonitorCard isHovered={isHoveringMonitors} />
<p className="text-xs text-neutral-300 leading-tight"> </motion.div>
Tech Lead & Scrum Master. Orchestrating sprint cycles, system design, and cross-functional team growth.
</p>
</section>
<section>
<h4 className="text-[12px] font-mono text-purple-500 uppercase tracking-[0.2em] mb-2">Integrity</h4>
<p className="text-xs text-neutral-300 leading-tight">
Experienced in <span className="italic">High-Stakes Environments</span> (Medical/Regulatory), QMS, and Cyber Essentials.
</p>
</section>
<section>
<h4 className="text-[12px] font-mono text-green-500 uppercase tracking-[0.2em] mb-2">Infrastructure</h4>
<p className="text-xs text-neutral-300 leading-tight">
Kubernetes, GCP, and automated CI/CD pipelines.
</p>
</section>
</div>
</div>
</div>
{/* Top Row Right: The Service Registry (Restored) */}
<motion.div
whileHover={{ y: -5 }}
className="group md:col-span-2 p-6 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-center relative overflow-hidden transition-all duration-300 min-h-[180px]"
>
{/* The Monitor Map (Easily editable) */}
{(() => {
const monitors = [
{ id: 2, name: "Datasaur" },
{ id: 6, name: "Audiobookshelf" },
{ id: 7, name: "Woodpecker CI" },
{ id: 8, name: "Forgejo Git" },
{ id: 9, name: "Server dashboard" },
{ id: 10, name: "Ratoong" },
];
return (
<>
{/* Default View */}
<div className="flex min-h-[250px] items-center justify-between w-full group-hover:opacity-0 group-hover:pointer-events-none transition-opacity duration-300">
<div>
<div className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="font-medium text-white">Hetzner Node-01</span>
</div>
<div className="flex gap-2 items-center">
<p className="text-sm text-neutral-500">System Status:</p>
<img
src="https://status.georgew.dev/api/status-page/dashboard/badge"
alt="Overall Status"
className="h-5"
/>
</div>
</div>
<Activity className="text-neutral-700 w-8 h-8" />
</div>
{/* Hover View: Friendly Names */}
<div className="absolute inset-0 p-6 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-center bg-neutral-900/95 backdrop-blur-sm">
<h4 className="text-[12px] font-mono text-neutral-500 mb-3 uppercase tracking-[0.2em]">Service Registry</h4>
<div className="grid grid-cols-1 sm:grid-cols-1 gap-2">
{monitors.map((m) => (
<div key={m.id} className="flex items-center justify-between bg-neutral-800/40 p-2 rounded-lg border border-neutral-700/30">
<span className="text-[11px] font-medium text-neutral-300 truncate mr-2">{m.name}</span>
<div className="flex gap-1 shrink-0">
<img src={`https://status.georgew.dev/api/badge/${m.id}/status`} className="h-4" alt="up" />
<img src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`} className="h-4 opacity-60" alt="ms" />
</div>
</div>
))}
</div>
</div>
</>
);
})()}
</motion.div>
{/* Middle Row: Web Systems */} {/* Middle Row: Web Systems */}
<Link href="/projects/web" className="group md:col-span-2"> <Link href="/projects/web" className="group md:col-span-2">
@ -153,15 +132,21 @@ export default function Home() {
<Globe className="text-blue-400 w-6 h-6 mb-4" /> <Globe className="text-blue-400 w-6 h-6 mb-4" />
<h3 className="font-bold text-xl mb-2">Web Systems</h3> <h3 className="font-bold text-xl mb-2">Web Systems</h3>
<p className="text-sm text-neutral-500 leading-relaxed"> <p className="text-sm text-neutral-500 leading-relaxed">
Architecting distributed platforms with a focus on high-availability and containerized deployment. Architecting distributed platforms with a focus on
high-availability and containerized deployment.
</p> </p>
</div> </div>
<div className="flex flex-wrap gap-2 mt-6"> <div className="flex flex-wrap gap-2 mt-6">
{['Next.js', 'Python', 'Node.js', 'Caddy', 'PostgreSQL'].map(tech => ( {["Next.js", "Python", "Node.js", "Caddy", "PostgreSQL"].map(
<span key={tech} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-blue-400 group-hover:border-blue-500/20 transition-all"> (tech) => (
{tech} <span
</span> key={tech}
))} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-blue-400 group-hover:border-blue-500/20 transition-all"
>
{tech}
</span>
),
)}
</div> </div>
</motion.div> </motion.div>
</Link> </Link>
@ -176,15 +161,21 @@ export default function Home() {
<Smartphone className="text-purple-400 w-6 h-6 mb-4" /> <Smartphone className="text-purple-400 w-6 h-6 mb-4" />
<h3 className="font-bold text-xl mb-2">Mobile Apps</h3> <h3 className="font-bold text-xl mb-2">Mobile Apps</h3>
<p className="text-sm text-neutral-500 leading-relaxed"> <p className="text-sm text-neutral-500 leading-relaxed">
Building fluid, cross-platform experiences using reactive state and native hardware integration. Building fluid, cross-platform experiences using reactive
state and native hardware integration.
</p> </p>
</div> </div>
<div className="flex flex-wrap gap-2 mt-6"> <div className="flex flex-wrap gap-2 mt-6">
{['Android', 'iOS', 'Flutter', 'Riverpod', 'Stores'].map(tech => ( {["Android", "iOS", "Flutter", "Riverpod", "Stores"].map(
<span key={tech} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-purple-400 group-hover:border-purple-500/20 transition-all"> (tech) => (
{tech} <span
</span> key={tech}
))} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-purple-400 group-hover:border-purple-500/20 transition-all"
>
{tech}
</span>
),
)}
</div> </div>
</motion.div> </motion.div>
</Link> </Link>
@ -199,15 +190,21 @@ export default function Home() {
<Server className="text-green-400 w-6 h-6 mb-4" /> <Server className="text-green-400 w-6 h-6 mb-4" />
<h3 className="font-bold text-xl mb-2">DevOps</h3> <h3 className="font-bold text-xl mb-2">DevOps</h3>
<p className="text-sm text-neutral-500 leading-relaxed"> <p className="text-sm text-neutral-500 leading-relaxed">
Managing self-hosted cloud nodes with automated CI/CD pipelines and proactive monitoring. Managing self-hosted cloud nodes with automated CI/CD
pipelines and proactive monitoring.
</p> </p>
</div> </div>
<div className="flex flex-wrap gap-2 mt-6"> <div className="flex flex-wrap gap-2 mt-6">
{['Docker', 'Woodpecker', 'Hetzner', 'Linux', 'Uptime'].map(tech => ( {["Docker", "Woodpecker", "Hetzner", "Linux", "Uptime"].map(
<span key={tech} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-green-400 group-hover:border-green-500/20 transition-all"> (tech) => (
{tech} <span
</span> key={tech}
))} className="text-[9px] font-mono text-neutral-600 border border-neutral-800 px-2 py-1 rounded-md uppercase group-hover:text-green-400 group-hover:border-green-500/20 transition-all"
>
{tech}
</span>
),
)}
</div> </div>
</motion.div> </motion.div>
</Link> </Link>
@ -219,33 +216,34 @@ export default function Home() {
</div> </div>
<div> <div>
<h3 className="font-bold text-xl">The Forge</h3> <h3 className="font-bold text-xl">The Forge</h3>
<p className="text-sm text-neutral-500">Indie Game Dev & Creative Prototypes</p> <p className="text-sm text-neutral-500">
Indie Game Dev & Creative Prototypes
</p>
</div> </div>
</div> </div>
</div> </div>
<footer className="mt-12 pt-8 border-t border-neutral-900 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] text-neutral-600 font-mono tracking-wider uppercase"> <footer className="mt-12 pt-8 border-t border-neutral-900 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] text-neutral-600 font-mono tracking-wider uppercase">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p>Pipeline Status</p> <p>Pipeline Status</p>
<img <img
src="https://ci.georgew.dev/api/badges/11/status.svg" src="https://ci.georgew.dev/api/badges/11/status.svg"
alt="Build Status" alt="Build Status"
className="h-3 grayscale opacity-50 hover:opacity-100 hover:grayscale-0 transition-all" className="h-3 grayscale opacity-50 hover:opacity-100 hover:grayscale-0 transition-all"
/> />
</div>
<div className="hidden md:block w-[1px] h-3 bg-neutral-800" />
<p>Engine: Next.js 15 (Standalone)</p>
</div> </div>
<div className="hidden md:block w-[1px] h-3 bg-neutral-800" />
<p>Engine: Next.js 15 (Standalone)</p>
</div>
<div className="flex items-center gap-4 bg-neutral-900/50 px-3 py-1 rounded-full border border-neutral-800/50"> <div className="flex items-center gap-4 bg-neutral-900/50 px-3 py-1 rounded-full border border-neutral-800/50">
<div className="w-1 h-1 rounded-full bg-blue-500" /> <div className="w-1 h-1 rounded-full bg-blue-500" />
<p className="text-neutral-400"> <p className="text-neutral-400">
Deploy: {process.env.NEXT_PUBLIC_APP_VERSION || 'v1.0.0-dev'} Deploy: {process.env.NEXT_PUBLIC_APP_VERSION || "v1.0.0-dev"}
</p> </p>
</div> </div>
</footer> </footer>
</div> </div>
</main> </main>
); );
} }

View file

@ -0,0 +1,177 @@
"use client";
import { use } from "react";
import { motion } from "framer-motion";
import Link from "next/link";
import {
ArrowLeft,
ExternalLink,
Github,
ShieldCheck,
Cpu,
Users,
} from "lucide-react";
import { PROJECT_REGISTRY } from "@/data/projects";
import Mermaid from "@/components/Mermaid";
import ProjectShowcase from "@/components/ProjectShowcase";
import ImageCarousel from "@/components/ImageCarousel";
import ReactMarkdown from "react-markdown";
export default function ProjectDetail({
params,
}: {
params: Promise<{ category: string; slug: string }>;
}) {
const { category, slug } = use(params);
const project = PROJECT_REGISTRY.find((p) => p.slug === slug);
if (!project) return <div>Project Not Found</div>;
if (!project)
return (
<div className="p-24 text-white font-mono">Project Log Not Found.</div>
);
return (
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
<div className="max-w-6xl mx-auto">
{/* Navigation */}
<Link
href={`/projects/${category}`}
className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-[10px] uppercase tracking-widest"
>
<ArrowLeft size={12} /> Back to {category}
</Link>
{/* Header Section */}
<header className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-20">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
>
<h1 className="text-6xl font-bold tracking-tighter mb-4">
{project.title}
</h1>
<p className="text-blue-500 font-mono text-sm uppercase tracking-widest mb-6">
{project.subtitle}
</p>
<p className="text-neutral-400 text-lg leading-relaxed">
{project.description}
</p>
<div className="flex gap-4 mt-8">
{project.liveUrl && (
<a
href={project.liveUrl}
className="flex items-center gap-2 bg-white text-black px-6 py-3 rounded-full font-bold text-sm hover:bg-neutral-200 transition-all"
>
Launch Site <ExternalLink size={14} />
</a>
)}
{project?.repoUrl && (
<a
href={project.repoUrl}
className="flex items-center gap-2 border border-neutral-800 px-6 py-3 rounded-full font-bold text-sm hover:bg-neutral-900 transition-all"
>
View Source <Github size={14} />
</a>
)}
</div>
</motion.div>
{/* Stats Sidebar */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="bg-neutral-900/30 border border-neutral-800 p-8 rounded-3xl h-fit"
>
<div className="space-y-8">
<div className="flex items-center gap-4">
<ShieldCheck className="text-blue-500" />
<div>
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">
My Role
</p>
<p className="text-sm font-semibold">{project.role}</p>
</div>
</div>
<div className="flex items-center gap-4">
<Cpu className="text-purple-500" />
<div>
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">
Stack
</p>
<p className="text-sm font-semibold">
{project.stack.join(", ")}
</p>
</div>
</div>
<div className="flex items-center gap-4">
<Users className="text-green-500" />
<div>
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">
Impact
</p>
<p className="text-sm font-semibold">
{project.metrics.join(" • ")}
</p>
</div>
</div>
</div>
</motion.div>
</header>
<section className="mb-20">
{/* Desktop Showcase View */}
<div className="hidden lg:block">
<ProjectShowcase images={project.images} />
</div>
{/* Mobile Carousel View */}
<div className="block lg:hidden">
<ImageCarousel images={project.images} />
</div>
<p className="mt-4 text-center text-[10px] font-mono text-neutral-600 uppercase tracking-[0.2em]">
Interactive Gallery Select or swipe to explore
</p>
</section>
{/* Mermaid */}
{project.mermaidChart && (
<section className="mb-16">
<div className="flex items-center gap-3 mb-8">
<div className="h-px flex-1 bg-neutral-900" />
<h3 className="text-[10px] font-mono text-neutral-500 uppercase tracking-[0.3em]">
System Architecture Log
</h3>
<div className="h-px flex-1 bg-neutral-900" />
</div>
<div className="bg-neutral-900/20 rounded-3xl border border-neutral-800/50 p-4 md:p-12">
<Mermaid chart={project.mermaidChart} />
</div>
</section>
)}
{/* Engineering Narrative */}
<section className="w-full pb-20 mt-12">
<div className="flex items-center gap-3 mb-12">
<h3 className="text-[10px] font-mono text-blue-500 uppercase tracking-[0.3em] font-bold">
PROJECT LOG // {project.storyLabel || "NARRATIVE"}
</h3>
<div className="h-px flex-1 bg-neutral-900" />
<h2 className="text-xl font-bold tracking-tighter">
The Engineering Story
</h2>
</div>
<div className="prose prose-invert prose-neutral max-w-none text-left">
<ReactMarkdown>{project.engineeringStory}</ReactMarkdown>
</div>
</section>
</div>
</main>
);
}

View file

@ -1,70 +1,62 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import Link from "next/link"; import Link from "next/link";
import { Globe, Smartphone, ArrowLeft } from "lucide-react"; import { Globe, Smartphone, ArrowLeft, Server } from "lucide-react";
import { use } from "react"; import { use } from "react";
import { PROJECT_REGISTRY } from "@/data/projects";
const categories = { const CATEGORY_META = {
web: { web: {
title: "Web Systems", title: "Web Systems",
icon: <Globe className="w-8 h-8 text-blue-400" />, icon: <Globe className="w-8 h-8 text-blue-400" />,
description: "Architecting scalable web applications and distributed systems.", description:
projects: [ "Architecting scalable web applications and distributed systems.",
{
name: "Ratoong",
detail: "Professional production platform.",
stack: ["Node.js", "PostgreSQL", "Caddy"]
},
{
name: "Datasaur",
detail: "Full-stack data science pipeline.",
stack: ["Python", "FastAPI", "Next.js"]
}
]
}, },
mobile: { mobile: {
title: "Mobile Apps", title: "Mobile Apps",
icon: <Smartphone className="w-8 h-8 text-purple-400" />, icon: <Smartphone className="w-8 h-8 text-purple-400" />,
description: "Building cross-platform experiences with Flutter and native integrations.", description:
projects: [ "Building cross-platform experiences with Flutter and native integrations.",
{ },
name: "Flutter App 1", infrastructure: {
detail: "Active Development - Coming Soon", title: "DevOps & Infrastructure",
stack: ["Flutter", "Dart", "Firebase"] icon: <Server className="w-8 h-8 text-green-400" />,
}, description:
{ "Self-hosted systems architecture and automated deployment pipelines.",
name: "Flutter App 2", },
detail: "Internal R&D Prototype",
stack: ["Flutter", "Riverpod", "SQLite"]
}
]
}
}; };
export default function CategoryPage({ params }: { params: Promise<{ category: string }> }) { export default function CategoryPage({
params,
}: {
params: Promise<{ category: string }>;
}) {
const resolvedParams = use(params); const resolvedParams = use(params);
const category = resolvedParams.category; const category = resolvedParams.category;
const data = categories[category as keyof typeof categories]; const meta = CATEGORY_META[category as keyof typeof CATEGORY_META];
if (!data) return <div className="p-24 text-white">Category not found.</div>; const filteredProjects = PROJECT_REGISTRY.filter(
(p) => p.category === category,
);
if (!meta) {
if (!data) {
return ( return (
<div className="p-24 text-white font-mono"> <div className="p-24 text-white font-mono">
<h1 className="text-2xl mb-4">404: Category Not Found</h1> <h1 className="text-2xl mb-4">404: Category Not Found</h1>
<p className="text-neutral-500">Path: /projects/{category}</p> <Link href="/" className="text-blue-400 underline mt-4 block">
<Link href="/" className="text-blue-400 underline mt-4 block">Return Home</Link> Return Home
</Link>
</div> </div>
); );
} }
return ( return (
<main className="min-h-screen bg-[#0a0a0a] text-white p-8 md:p-24"> <main className="min-h-screen bg-[#0a0a0a] text-white p-8 md:p-24">
<Link href="/" className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-xs"> <Link
href="/"
className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-xs"
>
<ArrowLeft size={14} /> BACK TO DASHBOARD <ArrowLeft size={14} /> BACK TO DASHBOARD
</Link> </Link>
@ -74,40 +66,51 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
className="max-w-5xl mx-auto" className="max-w-5xl mx-auto"
> >
<div className="flex items-center gap-4 mb-6"> <div className="flex items-center gap-4 mb-6">
{data.icon} {meta.icon}
<h1 className="text-5xl font-bold">{data.title}</h1> <h1 className="text-5xl font-bold">{meta.title}</h1>
</div> </div>
<p className="text-xl text-neutral-400 max-w-2xl mb-16">{data.description}</p> <p className="text-xl text-neutral-400 max-w-2xl mb-16">
{meta.description}
</p>
<div className="grid grid-cols-1 gap-8"> <div className="grid grid-cols-1 gap-8">
{data.projects.map((project, index) => ( {filteredProjects.map((project, index) => (
<motion.div <Link
key={project.name} key={project.slug}
layout href={`/projects/${category}/${project.slug}`}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{
delay: index * 0.1,
duration: 0.4,
ease: "easeOut"
}}
className="group p-8 rounded-3xl bg-neutral-900/50 border border-neutral-800 hover:border-neutral-700 transition-colors"
> >
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> <motion.div
<div> layout
<h3 className="text-2xl font-semibold mb-2">{project.name}</h3> initial={{ opacity: 0, x: -20 }}
<p className="text-neutral-500">{project.detail}</p> whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{
delay: index * 0.1,
duration: 0.4,
ease: "easeOut",
}}
className="group p-8 rounded-3xl bg-neutral-900/50 border border-neutral-800 hover:border-neutral-700 transition-colors"
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h3 className="text-2xl font-semibold mb-2">
{project.title}
</h3>
<p className="text-neutral-500">{project.description}</p>
</div>
<div className="flex flex-wrap gap-2">
{project.stack.map((s) => (
<span
key={s}
className="text-[10px] font-mono bg-neutral-800 text-neutral-400 px-3 py-1 rounded-full uppercase"
>
{s}
</span>
))}
</div>
</div> </div>
<div className="flex flex-wrap gap-2"> </motion.div>
{project.stack.map(s => ( </Link>
<span key={s} className="text-[10px] font-mono bg-neutral-800 text-neutral-400 px-3 py-1 rounded-full uppercase">
{s}
</span>
))}
</div>
</div>
</motion.div>
))} ))}
</div> </div>
</motion.div> </motion.div>

View file

@ -0,0 +1,112 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";
interface GalleryProps {
images: string[];
}
export default function ImageCarousel({ images }: GalleryProps) {
const [[page, direction], setPage] = useState([0, 0]);
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
const imageIndex = Math.abs(page % images.length);
const paginate = useCallback(
(newDirection: number) => {
setPage([page + newDirection, newDirection]);
},
[page],
);
useEffect(() => {
if (!isAutoPlaying) return;
const interval = setInterval(() => {
paginate(1);
}, 5000);
return () => clearInterval(interval);
}, [paginate, isAutoPlaying]);
const variants = {
enter: (direction: number) => ({
x: direction > 0 ? 1000 : -1000,
opacity: 0,
}),
center: { zIndex: 1, x: 0, opacity: 1 },
exit: (direction: number) => ({
zIndex: 0,
x: direction < 0 ? 1000 : -1000,
opacity: 0,
}),
};
return (
<div
className="relative aspect-video w-full overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900"
onMouseEnter={() => setIsAutoPlaying(false)}
onMouseLeave={() => setIsAutoPlaying(true)}
>
<AnimatePresence initial={false} custom={direction}>
<motion.img
key={page}
src={images[imageIndex]}
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
x: { type: "spring", stiffness: 300, damping: 30 },
opacity: { duration: 0.2 },
}}
drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={1}
onDragStart={() => setIsAutoPlaying(false)}
onDragEnd={(e, { offset }) => {
const swipe = Math.abs(offset.x) > 50;
if (swipe) paginate(offset.x > 0 ? -1 : 1);
}}
className="absolute h-full w-full object-cover cursor-grab active:cursor-grabbing"
/>
</AnimatePresence>
{/* Navigation Arrows */}
<div className="absolute inset-0 z-10 flex items-center justify-between p-4 pointer-events-none">
<button
className="p-2 rounded-full bg-black/50 backdrop-blur-md border border-white/10 text-white pointer-events-auto hover:bg-black/80 transition-all"
onClick={() => {
setIsAutoPlaying(false);
paginate(-1);
}}
>
<ChevronLeft size={24} />
</button>
<button
className="p-2 rounded-full bg-black/50 backdrop-blur-md border border-white/10 text-white pointer-events-auto hover:bg-black/80 transition-all"
onClick={() => {
setIsAutoPlaying(false);
paginate(1);
}}
>
<ChevronRight size={24} />
</button>
</div>
{/* Progress Bar */}
{isAutoPlaying && (
<motion.div
key={imageIndex}
initial={{ width: "0%" }}
animate={{ width: "100%" }}
transition={{ duration: 5, ease: "linear" }}
className="absolute bottom-0 left-0 h-1 bg-blue-500/50 z-20"
/>
)}
</div>
);
}

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>
);
}

98
components/Mermaid.tsx Normal file
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>
);
}

136
components/MonitorCard.tsx Normal file
View file

@ -0,0 +1,136 @@
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Activity } from "lucide-react";
import Image from "next/image";
const MONITORS = [
{ id: 2, name: "Datasaur" },
{ 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" },
];
const ITEMS_PER_PAGE = 6;
export default function MonitorRegistry({ isHovered }: { isHovered: boolean }) {
const [page, setPage] = useState(0);
const totalPages = Math.ceil(MONITORS.length / ITEMS_PER_PAGE);
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (isHovered && totalPages > 1) {
interval = setInterval(() => {
setPage((prev) => (prev + 1) % totalPages);
}, 4000);
}
return () => {
if (interval) clearInterval(interval);
if (!isHovered) {
setPage(0);
}
};
}, [isHovered, totalPages]);
const currentMonitors = MONITORS.slice(
page * ITEMS_PER_PAGE,
(page + 1) * ITEMS_PER_PAGE,
);
return (
<>
{/* Default View */}
<div className="flex min-h-[250px] flex-col justify-center w-full group-hover:opacity-0 group-hover:pointer-events-none transition-all duration-300">
{/* Header Section */}
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]" />
<span className="font-medium text-white tracking-tight">
Hetzner Node-01
</span>
</div>
<div className="flex gap-2 items-center">
<p className="text-xs text-neutral-500 font-mono">SYS_STATUS:</p>
<span className="text-[10px] bg-green-500/10 text-green-500 border border-green-500/20 px-1.5 py-0.5 rounded uppercase font-bold">
Online
</span>
</div>
</div>
<Activity className="text-neutral-800 w-10 h-10 -mt-1" />
</div>
<div className="mt-8 grid grid-cols-2 gap-4 border-t border-neutral-800/50 pt-6">
<div>
<p className="text-[10px] font-mono text-neutral-600 uppercase">
Architecture
</p>
<p className="text-xs text-neutral-400">linux/amd64</p>
</div>
<div>
<p className="text-[10px] font-mono text-neutral-600 uppercase">
Provider
</p>
<p className="text-xs text-neutral-400">Hetzner Cloud</p>
</div>
</div>
</div>
{/* Hover View */}
<div className="absolute inset-0 p-5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col bg-neutral-900/95 backdrop-blur-sm">
<div className="flex justify-between items-center mb-2 px-1">
<h4 className="text-[10px] font-mono text-neutral-500 uppercase tracking-[0.2em]">
Service Registry
</h4>
</div>
<div className="flex-1 relative min-h-0 overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key={page}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.4 }}
className="grid grid-cols-1 gap-2"
>
{currentMonitors.map((m) => (
<div
key={m.id}
className="flex items-center justify-between bg-neutral-800/30 p-1.5 px-3 rounded-lg border border-neutral-700/30"
>
<span className="text-[12px] font-medium text-neutral-300 truncate mr-2">
{m.name}
</span>
<div className="flex gap-1 shrink-0 scale-90 origin-right">
<img
src={`https://status.georgew.dev/api/badge/${m.id}/status`}
className="h-5"
alt="up"
/>
<img
src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`}
className="h-5 opacity-60"
alt="ms"
/>
</div>
</div>
))}
</motion.div>
</AnimatePresence>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,62 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
export default function ProjectShowcase({ images }: { images: string[] }) {
const [index, setIndex] = useState(0);
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 h-[500px]">
{/* Main Image */}
<div className="lg:col-span-9 relative overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900 group">
<AnimatePresence mode="wait">
<motion.img
key={index}
src={images[index]}
initial={{ opacity: 0, scale: 1.05 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.4, ease: "easeInOut" }}
className="absolute inset-0 h-full w-full object-cover"
/>
</AnimatePresence>
{/* Subtle Overlay Label */}
<div className="absolute bottom-4 left-4 bg-black/60 backdrop-blur-md px-3 py-1.5 rounded-full border border-white/10 opacity-0 group-hover:opacity-100 transition-opacity">
<p className="text-[10px] font-mono uppercase tracking-widest text-white/70">
View {index + 1} of {images.length}
</p>
</div>
</div>
{/* Thumbnail Column */}
<div className="lg:col-span-3 flex lg:flex-col gap-3 overflow-x-auto lg:overflow-y-auto pr-2 custom-scrollbar">
{images.map((img, i) => (
<button
key={i}
onClick={() => setIndex(i)}
className={`relative flex-shrink-0 w-24 lg:w-full aspect-video rounded-xl border-2 transition-all overflow-hidden ${
i === index
? "border-blue-500 ring-4 ring-blue-500/10"
: "border-neutral-800 opacity-40 hover:opacity-100"
}`}
>
<img
src={img}
className="h-full w-full object-cover"
alt={`Thumb ${i}`}
/>
{i === index && (
<motion.div
layoutId="active-thumb"
className="absolute inset-0 bg-blue-500/10 z-10"
/>
)}
</button>
))}
</div>
</div>
);
}

285
data/projects.ts Normal file
View file

@ -0,0 +1,285 @@
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",
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
Leveraging a **Document-Based Architecture (Firestore)**, I designed a schema that balanced read efficiency with real-time updates. To handle complex filtering (altitude, lift types, pricing) without taxing the client-side, I utilized **GCP Cloud Functions** as a middleware layer to process and normalize data from various 3rd-party APIs, including Google Maps and Weather services.
#### Modern Angular & Responsive UI
The frontend was built using modern **Angular**, focusing on a component-based architecture that ensured high performance across both desktop and mobile. I implemented a custom state management flow to handle resort ratings and trip planning, ensuring that user interactions were instantly reflected in the UI while syncing seamlessly with **Firebase Authentication**.
#### Lessons in Scalability
Working with a **Backend-as-a-Service (BaaS)** model taught me the importance of cost-effective query design and the power of event-driven triggers. I was responsible for maintaining the development, staging, and production environments, ensuring a clean CI/CD flow from localhost to the Firebase cloud.
#### Security & Data Governance
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",
],
liveUrl: "https://ratoong.com",
isPrivate: false,
mermaidChart: `
graph LR
subgraph Client_Side [Frontend]
A[Angular Web App]:::traffic
end
subgraph Firebase_GCP [Cloud Infrastructure]
direction TB
Hub((Firebase SDK)):::hub
B[Firebase Auth]:::node
C[Firestore DB]:::node
D[Cloud Functions]:::node
E[Partner API Proxy]:::node
end
subgraph External [Third Party]
direction TB
F[Weather API]:::traffic
G[Google Maps API]:::traffic
H[Affiliate Partners]:::traffic
end
A ==> Hub
Hub -->|Identity| B
Hub <-->|Data Sync| C
Hub -->|Triggers| D
D --> F
D --> G
D --> H
E -.->|Internal Access| C
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
classDef hub fill:#f59e0b,stroke:#d97706,color:#fff,stroke-width:2px
`,
storyLabel: "DATA // UI EFFICIENCY",
},
{
slug: "datasaur",
category: "web",
title: "Datasaur",
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://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: "infrastructure",
title: "Ayla",
subtitle: "Regulatory-Compliant Medical Platform",
role: "Tech Lead & Scrum Master",
duration: "2022 — 2024",
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,
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",
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"],
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,
},
{
slug: "flutter-2",
category: "mobile",
title: "Flutter-1",
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"],
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,
},
];

View file

@ -1,18 +1,13 @@
import { defineConfig, globalIgnores } from "eslint/config"; import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals"; import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript"; import nextTs from "eslint-config-next/typescript";
import eslintConfigPrettier from "eslint-config-prettier";
const eslintConfig = defineConfig([ const eslintConfig = defineConfig([
...nextVitals, ...nextVitals,
...nextTs, ...nextTs,
// Override default ignores of eslint-config-next. eslintConfigPrettier,
globalIgnores([ globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]); ]);
export default eslintConfig; export default eslintConfig;

3172
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,20 +9,26 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/upgrade": "^4.1.18",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"mermaid": "^11.12.2",
"next": "16.1.4", "next": "16.1.4",
"postcss": "^8.5.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.4", "eslint-config-next": "16.1.4",
"tailwindcss": "^4", "eslint-config-prettier": "^10.1.8",
"react-markdown": "^10.1.0",
"tailwindcss": "^4.1.18",
"typescript": "^5" "typescript": "^5"
} }
} }

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

18
types/project.ts Normal file
View file

@ -0,0 +1,18 @@
export interface Project {
slug: string;
category: 'web' | 'mobile' | 'infrastructure';
title: string;
subtitle: string;
role: string;
duration: string;
stack: string[];
metrics: string[];
description: string;
engineeringStory: string;
storyLabel?: string;
images: string[];
liveUrl?: string;
repoUrl?: string;
mermaidChart?: string;
isPrivate: boolean;
}