Added project details pages. Cleaned up dashboard

This commit is contained in:
GeorgeWebberley 2026-01-29 20:54:37 +01:00
parent b0f5d62e3e
commit 5d0a86645d
9 changed files with 1745 additions and 97 deletions

View file

@ -3,8 +3,12 @@
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, Activity } from 'lucide-react';
import { useState } from 'react';
import MonitorCard from '@/components/MonitorCard';
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">
@ -19,9 +23,6 @@ export default function Home() {
<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>
@ -89,58 +90,11 @@ export default function Home() {
{/* Top Row Right: The Service Registry (Restored) */} {/* Top Row Right: The Service Registry (Restored) */}
<motion.div <motion.div
whileHover={{ y: -5 }} whileHover={{ y: -5 }}
onMouseEnter={() => setIsHoveringMonitors(true)}
onMouseLeave={() => setIsHoveringMonitors(false)}
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]" 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) */} <MonitorCard isHovered={isHoveringMonitors} />
{(() => {
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> </motion.div>
{/* Middle Row: Web Systems */} {/* Middle Row: Web Systems */}

View file

@ -0,0 +1,126 @@
"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";
// This would eventually move to a separate data file
const PROJECT_DETAILS = {
ratoong: {
title: "Ratoong",
subtitle: "Regulatory-Compliant Data Platform",
role: "Lead Full-Stack Engineer",
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. Focused on immutable audit trails and secure data orchestration.",
images: ["/ratoong-1.jpg", "/ratoong-2.jpg"], // Place these in /public
liveUrl: "https://ratoong.com",
repoUrl: "https://git.georgew.dev/georgew/datasaur",
},
datasaur: {
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"],
repoUrl: "https://git.georgew.dev/georgew/datasaur",
liveUrl: "https://ratoong.com",
}
};
export default function ProjectDetail({ params }: { params: Promise<{ category: string, slug: string }> }) {
const { category, slug } = use(params);
const project = PROJECT_DETAILS[slug as keyof typeof PROJECT_DETAILS];
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>
{/* Senior 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>
{/* Image Gallery */}
<section className="mb-20">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{project.images.map((img, i) => (
<motion.div key={i} whileHover={{ scale: 1.01 }} className="aspect-video bg-neutral-900 rounded-2xl border border-neutral-800 overflow-hidden relative">
{/* Placeholder for your actual images */}
<div className="absolute inset-0 flex items-center justify-center text-neutral-700 font-mono text-xs italic">
[Screenshot: {project.title} - Layer {i + 1}]
</div>
</motion.div>
))}
</div>
</section>
{/* Engineering Narrative */}
<article className="max-w-3xl border-t border-neutral-900 pt-16">
<h2 className="text-2xl font-bold mb-6 italic underline decoration-blue-500 underline-offset-8">The Engineering Story</h2>
<div className="prose prose-invert prose-neutral max-w-none text-neutral-400 leading-loose">
{/* Content drafted below */}
<p>Detail how you approached the architecture, the specific trade-offs you made, and how you ensured regulatory compliance in the data layer.</p>
</div>
</article>
</div>
</main>
);
}

View file

@ -0,0 +1,120 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
import { Globe, Smartphone, ArrowLeft } from "lucide-react";
import { use } from "react";
const categories = {
web: {
title: "Web Systems",
icon: <Globe className="w-8 h-8 text-blue-400" />,
description: "Architecting scalable web applications and distributed systems.",
projects: [
{
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: {
title: "Mobile Apps",
icon: <Smartphone className="w-8 h-8 text-purple-400" />,
description: "Building cross-platform experiences with Flutter and native integrations.",
projects: [
{
name: "Flutter App 1",
detail: "Active Development - Coming Soon",
stack: ["Flutter", "Dart", "Firebase"]
},
{
name: "Flutter App 2",
detail: "Internal R&D Prototype",
stack: ["Flutter", "Riverpod", "SQLite"]
}
]
}
};
export default function CategoryPage({ params }: { params: Promise<{ category: string }> }) {
const resolvedParams = use(params);
const category = resolvedParams.category;
const data = categories[category as keyof typeof categories];
if (!data) return <div className="p-24 text-white">Category not found.</div>;
if (!data) {
return (
<div className="p-24 text-white font-mono">
<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">Return Home</Link>
</div>
);
}
return (
<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">
<ArrowLeft size={14} /> BACK TO DASHBOARD
</Link>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="max-w-5xl mx-auto"
>
<div className="flex items-center gap-4 mb-6">
{data.icon}
<h1 className="text-5xl font-bold">{data.title}</h1>
</div>
<p className="text-xl text-neutral-400 max-w-2xl mb-16">{data.description}</p>
<div className="grid grid-cols-1 gap-8">
{data.projects.map((project, index) => (
<Link
key={project.name}
href={`/projects/${category}/${project.name.toLowerCase()}`}>
<motion.div
layout
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">
<div>
<h3 className="text-2xl font-semibold mb-2">{project.name}</h3>
<p className="text-neutral-500">{project.detail}</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>
</motion.div>
</Link>
))}
</div>
</motion.div>
</main>
);
}

View file

@ -1,43 +1,25 @@
"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: "Architecting scalable web applications and distributed systems.",
projects: [
{
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: "Building cross-platform experiences with Flutter and native integrations.",
projects: [
{
name: "Flutter App 1",
detail: "Active Development - Coming Soon",
stack: ["Flutter", "Dart", "Firebase"]
}, },
{ infrastructure: {
name: "Flutter App 2", title: "DevOps & Infrastructure",
detail: "Internal R&D Prototype", icon: <Server className="w-8 h-8 text-green-400" />,
stack: ["Flutter", "Riverpod", "SQLite"] description: "Self-hosted systems architecture and automated deployment pipelines.",
}
]
} }
}; };
@ -47,21 +29,22 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
const resolvedParams = use(params); const resolvedParams = use(params);
const category = resolvedParams.category; const category = resolvedParams.category;
const data = categories[category as keyof typeof categories]; // 1. Get metadata for the header
const meta = CATEGORY_META[category as keyof typeof CATEGORY_META];
if (!data) return <div className="p-24 text-white">Category not found.</div>; // 2. Filter the registry to find projects belonging to this category
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">Return Home</Link> <Link href="/" className="text-blue-400 underline mt-4 block">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">
@ -74,15 +57,18 @@ 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) => (
<Link
key={project.slug}
href={`/projects/${category}/${project.slug}`}>
<motion.div <motion.div
key={project.name}
layout layout
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }} whileInView={{ opacity: 1, x: 0 }}
@ -96,8 +82,8 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
> >
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div> <div>
<h3 className="text-2xl font-semibold mb-2">{project.name}</h3> <h3 className="text-2xl font-semibold mb-2">{project.title}</h3>
<p className="text-neutral-500">{project.detail}</p> <p className="text-neutral-500">{project.description}</p>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{project.stack.map(s => ( {project.stack.map(s => (
@ -108,6 +94,7 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
</div> </div>
</div> </div>
</motion.div> </motion.div>
</Link>
))} ))}
</div> </div>
</motion.div> </motion.div>

117
components/MonitorCard.tsx Normal file
View file

@ -0,0 +1,117 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Activity } from 'lucide-react';
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) {
// Only start the interval if we are hovered
interval = setInterval(() => {
setPage((prev) => (prev + 1) % totalPages);
}, 4000);
}
// This cleanup function runs whenever isHovered changes
// or the component unmounts.
return () => {
if (interval) clearInterval(interval);
// Move the reset here so it happens "after" the effect cycle
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>
{/* Added "Server Specs" to fill space and match the style of "The Architect" */}
<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 */}
{/* Hover View: Automated Carousel */}
<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>
{/* Increased height slightly to 200px and removed 'justify-center' from parent */}
<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" // Reduced gap from 2 to 1.5
>
{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"> {/* Scale badges slightly */}
<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>
</>
);
}

82
data/projects.ts Normal file
View file

@ -0,0 +1,82 @@
import { Project } from "@/types/project";
export const PROJECT_REGISTRY: Project[] = [
{
slug: "ratoong",
category: "web",
title: "Ratoong",
subtitle: "Regulatory-Compliant Data Platform",
role: "Lead Full-Stack Engineer",
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",
isPrivate: true,
},
{
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"],
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...",
isPrivate: true,
},
{
slug: "ayla",
category: "web",
title: "Ayla",
subtitle: "Regulatory-Compliant Data Platform",
role: "Lead Full-Stack Engineer",
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",
isPrivate: true,
},
{
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...",
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...",
isPrivate: true,
},
];

1247
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"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",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"

16
types/project.ts Normal file
View file

@ -0,0 +1,16 @@
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;
images: string[];
liveUrl?: string;
repoUrl?: string;
isPrivate: boolean;
}