+ {/* Specification List */}
+
{CAPABILITIES.map((cap, i) => (
@@ -95,11 +98,11 @@ export default function InfrastructurePage() {
))}
- {/* Corrected Alignment Featured Section */}
-
+ {/* Featured Case Study Section */}
+
-
+
Selected Case Study
@@ -131,6 +134,6 @@ export default function InfrastructurePage() {
-
+
);
}
diff --git a/app/page.tsx b/app/page.tsx
index 30e2e51..cb7780f 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,19 +1,46 @@
"use client";
import Link from "next/link";
-import { motion } from "framer-motion";
+import { motion, Variants } from "framer-motion";
import { Globe, Smartphone, Server, Gamepad2 } from "lucide-react";
import { useState } from "react";
import MonitorCard from "@/components/MonitorCard";
-import Image from "next/image";
+import PageLayout from "@/components/PageLayout";
+import { CategoryCardProps } from "@/types/index";
+
+const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.1,
+ delayChildren: 0.2,
+ },
+ },
+};
+
+const itemVariants: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: "easeOut" },
+ },
+};
export default function Home() {
const [isHoveringMonitors, setIsHoveringMonitors] = useState(false);
return (
-
-
-
+
+
+ {/* Header Section */}
+
George W.
Senior Full Stack Engineer & Tech Lead
@@ -32,14 +59,15 @@ export default function Home() {
LinkedIn
-
+
+ {/* Main Bento Grid */}
{/* Top Row Left: The Architect */}
-
-
-
- {/* Description and tags */}
+
@@ -76,142 +104,74 @@ export default function Home() {
- {/* Technical details */}
-
-
- Leadership
-
-
- Tech Lead & Scrum Master. Orchestrating sprint cycles,
- system design, and cross-functional team growth.
-
-
-
-
-
- Integrity
-
-
- Experienced in{" "}
- High-Stakes Environments{" "}
- (Medical/Regulatory), QMS, and Cyber Essentials.
-
-
-
-
-
- Infrastructure
-
-
- Kubernetes, GCP, and automated CI/CD pipelines.
-
-
+
+
+
-
+
{/* Top Row Right: The Service Registry */}
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 min-h-[180px]"
>
- {/* Middle Row: Web Systems */}
-
-
-
-
-
Web Systems
-
- Architecting distributed platforms with a focus on
- high-availability and containerized deployment.
-
-
-
- {["Next.js", "Python", "Node.js", "Caddy", "PostgreSQL"].map(
- (tech) => (
-
- {tech}
-
- ),
- )}
-
-
-
+ {/* Project Category Cards */}
+
}
+ title="Web Systems"
+ description="Architecting distributed platforms with a focus on high-availability."
+ tech={["Next.js", "Python", "Node.js", "Caddy", "PostgreSQL"]}
+ hoverColor="hover:border-blue-500/30"
+ activeTechColor="group-hover:text-blue-400"
+ />
- {/* Middle Row: Mobile Apps */}
-
-
-
-
-
Mobile Apps
-
- Building fluid, cross-platform experiences using reactive
- state and native hardware integration.
-
-
-
- {["Android", "iOS", "Flutter", "Riverpod", "Stores"].map(
- (tech) => (
-
- {tech}
-
- ),
- )}
-
-
-
+
}
+ title="Mobile Apps"
+ description="Building fluid, cross-platform experiences using reactive state."
+ tech={["Android", "iOS", "Flutter", "Riverpod", "Stores"]}
+ hoverColor="hover:border-purple-500/30"
+ activeTechColor="group-hover:text-purple-400"
+ />
- {/* Middle Row: DevOps */}
-
-
-
-
-
Infrastructure
-
- Architecting resilient cloud environments with automated IaC,
- multi-region orchestration, and high-integrity security
- protocols.
-
-
-
- {["Kubernetes", "Terraform", "GCP", "CI/CD", "Security"].map(
- (tech) => (
-
- {tech}
-
- ),
- )}
-
-
-
+
}
+ title="Infrastructure"
+ description="Resilient cloud environments with automated IaC and multi-region orchestration."
+ tech={["Kubernetes", "Terraform", "GCP", "CI/CD", "Security"]}
+ hoverColor="hover:border-green-500/30"
+ activeTechColor="group-hover:text-green-400"
+ />
{/* Bottom Row: The Forge */}
-
+
@@ -221,30 +181,68 @@ export default function Home() {
Indie Game Dev & Creative Prototypes
-
+
-
-
-
+
+
+ );
+}
+
+function TechnicalFocus({
+ label,
+ color,
+ text,
+}: {
+ label: string;
+ color: string;
+ text: string;
+}) {
+ return (
+
+ );
+}
+
+function CategoryCard({
+ href,
+ icon,
+ title,
+ description,
+ tech,
+ hoverColor,
+ activeTechColor,
+}: CategoryCardProps) {
+ return (
+
+
+
+ {icon}
+
{title}
+
+ {description}
+
+
+
+ {tech.map((t: string) => (
+
+ {t}
+
+ ))}
+
+
+
);
}
diff --git a/app/projects/[category]/[slug]/page.tsx b/app/projects/[category]/[slug]/page.tsx
index 182c305..2d07570 100644
--- a/app/projects/[category]/[slug]/page.tsx
+++ b/app/projects/[category]/[slug]/page.tsx
@@ -1,22 +1,36 @@
"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 { motion, Variants } from "framer-motion";
+import { 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";
import MobileStack from "@/components/MobileStack";
+import PageLayout from "@/components/PageLayout";
+
+// 1. Professional Animation Variants
+const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.15, // Smoothly delay each section
+ delayChildren: 0.1,
+ },
+ },
+};
+
+const sectionVariants: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.8, ease: "easeOut" },
+ },
+};
export default function ProjectDetail({
params,
@@ -26,30 +40,31 @@ export default function ProjectDetail({
const { category, slug } = use(params);
const project = PROJECT_REGISTRY.find((p) => p.slug === slug);
- if (!project) return
Project Not Found
;
-
- if (!project)
+ if (!project) {
return (
-
Project Log Not Found.
+
+ Project Log Not Found.
+
);
+ }
return (
-
-
- {/* Navigation */}
-
-
Back to {category}
-
-
+
+
{/* Header Section */}
-
-
+
+
{project.title}
@@ -64,89 +79,66 @@ export default function ProjectDetail({
{project.liveUrl && (
Launch Site
)}
-
{project?.repoUrl && (
View Source
)}
-
+
{/* Stats Sidebar */}
-
-
-
-
-
-
- My Role
-
-
{project.role}
-
-
-
-
-
-
- Stack
-
-
- {project.stack.join(", ")}
-
-
-
-
-
-
-
- Impact
-
-
- {project.metrics.join(" • ")}
-
-
-
+
+
+ }
+ label="My Role"
+ value={project.role}
+ />
+ }
+ label="Stack"
+ value={project.stack.join(", ")}
+ />
+ }
+ label="Impact"
+ value={project.metrics.join(" • ")}
+ />
-
-
+
+
-
+ {/* Media Showcase */}
+
{project.category === "mobile" ? (
) : (
<>
- {/* Display on large screens */}
- {/* Display on small screens */}
>
)}
-
-
+
Interactive Gallery — Select or swipe to explore
-
+
- {/* Mermaid */}
+ {/* System Architecture */}
{project.mermaidChart && (
-
+
@@ -154,15 +146,17 @@ export default function ProjectDetail({
-
-
+
)}
{/* Engineering Narrative */}
-
+
PROJECT LOG // {project.storyLabel || "NARRATIVE"}
@@ -172,12 +166,34 @@ export default function ProjectDetail({
The Engineering Story
-
-
+
{project.engineeringStory}
-
-
-
+
+
+
+ );
+}
+
+// Small helper component to keep the JSX clean
+function StatItem({
+ icon,
+ label,
+ value,
+}: {
+ icon: React.ReactNode;
+ label: string;
+ value: string;
+}) {
+ return (
+
+ {icon}
+
+
+ {label}
+
+
{value}
+
+
);
}
diff --git a/app/projects/[category]/page.tsx b/app/projects/[category]/page.tsx
index 5c54150..7b720e6 100644
--- a/app/projects/[category]/page.tsx
+++ b/app/projects/[category]/page.tsx
@@ -1,9 +1,11 @@
"use client";
-import { motion } from "framer-motion";
-import Link from "next/link";
-import { Globe, Smartphone, ArrowLeft, Server } from "lucide-react";
+
import { use } from "react";
+import { motion, Variants } from "framer-motion";
+import Link from "next/link";
+import { Globe, Smartphone, Server } from "lucide-react";
import { PROJECT_REGISTRY } from "@/data/projects";
+import PageLayout from "@/components/PageLayout";
const CATEGORY_META = {
web: {
@@ -26,6 +28,27 @@ const CATEGORY_META = {
},
};
+// 1. Cast the container variants
+const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: { staggerChildren: 0.1 },
+ },
+};
+
+// 2. Cast the item variants
+const itemVariants: Variants = {
+ hidden: { opacity: 0, x: -20 },
+ visible: {
+ opacity: 1,
+ x: 0,
+ transition: {
+ duration: 0.4,
+ ease: "easeOut", // TypeScript now knows this is a valid Easing string
+ },
+ },
+};
export default function CategoryPage({
params,
}: {
@@ -33,87 +56,67 @@ export default function CategoryPage({
}) {
const resolvedParams = use(params);
const category = resolvedParams.category;
-
const meta = CATEGORY_META[category as keyof typeof CATEGORY_META];
-
const filteredProjects = PROJECT_REGISTRY.filter(
(p) => p.category === category,
);
- if (!meta) {
- return (
-
-
404: Category Not Found
-
- Return Home
-
-
- );
- }
+ if (!meta) return Sector not found.;
return (
-
-
- BACK TO DASHBOARD
-
-
-
-
- {meta.icon}
-
{meta.title}
-
-
+
+
+
+ {meta.icon} {meta.title}
+
+
{meta.description}
+
-
- {filteredProjects.map((project, index) => (
-
+ {filteredProjects.map((project) => (
+
+
-
-
-
-
- {project.title}
-
-
{project.description}
-
-
- {project.stack.map((s) => (
-
- {s}
-
- ))}
-
+
+
+
+ {project.title}
+
+
+ {project.description}
+
-
-
- ))}
-
+
+ {project.stack.map((tech) => (
+
+ {tech}
+
+ ))}
+
+
+
+
+ ))}
-
+
);
}
diff --git a/components/Footer.tsx b/components/Footer.tsx
new file mode 100644
index 0000000..19f4378
--- /dev/null
+++ b/components/Footer.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+export default function Footer() {
+ return (
+
+ );
+}
diff --git a/components/Mermaid copy 2.tsx b/components/Mermaid copy 2.tsx
deleted file mode 100644
index d658d79..0000000
--- a/components/Mermaid copy 2.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-"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
(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 (
-
-
!isExpanded && setIsExpanded(true)}
- >
- {/* Legend */}
-
-
- {/* Chart Area */}
-
-
- {/* Fade Overlay */}
-
- {!isExpanded && (
-
- )}
-
-
-
- {/* Toggle Button */}
-
-
- );
-}
diff --git a/components/Mermaid copy.tsx b/components/Mermaid copy.tsx
deleted file mode 100644
index a03d2cc..0000000
--- a/components/Mermaid copy.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-"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(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 (
-
-
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 */}
-
-
-
- {chart}
-
-
- {/* The "Fade to Darkness" Overlay (when expansion is needed) */}
-
- {needsExpansion && !isExpanded && (
-
- )}
-
-
-
- {/* Expand/Collapse Button (when expansion is needed) */}
- {needsExpansion && (
-
- )}
-
- );
-}
diff --git a/components/MobileStack copy.tsx b/components/MobileStack copy.tsx
deleted file mode 100644
index 901219c..0000000
--- a/components/MobileStack copy.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-"use client";
-import { useState } from "react";
-import { motion, AnimatePresence } from "framer-motion";
-import { ArrowRight } from "lucide-react";
-import { MobileFrame } from "./MobileFrame";
-
-export default function MobileStack({ images }: { images: string[] }) {
- const [currentIndex, setCurrentIndex] = useState(0);
-
- const DRAG_THRESHOLD = -150;
-
- const getRelativeIndex = (index: number) => {
- const len = images.length;
- return (index - currentIndex + len) % len;
- };
-
- const next = () => setCurrentIndex((prev) => (prev + 1) % images.length);
-
- return (
-
-
-
- {images.map((img, index) => {
- const relIndex = getRelativeIndex(index);
- const isTop = relIndex === 0;
-
- const xOffset = relIndex * 90;
-
- if (relIndex > 5) return null;
-
- return (
- {
- if (isTop && info.offset.x < DRAG_THRESHOLD) {
- next();
- }
- }}
- onDragEnd={(_, info) => {
- // Backup check for quick flicks
- if (isTop && info.offset.x < -100) {
- next();
- }
- }}
- onClick={() => !isTop && setCurrentIndex(index)}
- className="absolute"
- >
-
-
-
-
-
-
- );
- })}
-
-
- {/* Navigation Button */}
-
-
-
-
-
- );
-}
diff --git a/components/MonitorCard.tsx b/components/MonitorCard.tsx
index 2fa0c95..acb74e6 100644
--- a/components/MonitorCard.tsx
+++ b/components/MonitorCard.tsx
@@ -3,7 +3,6 @@
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" },
@@ -20,39 +19,13 @@ const MONITORS = [
{ 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,
- );
-
+export default function MonitorCard({ isHovered }: { isHovered: boolean }) {
return (
- <>
- {/* Default View */}
-
- {/* Header Section */}
+
+ {/* DEFAULT VIEW: Always rendered, opacity controlled by parent hover */}
+
@@ -61,12 +34,9 @@ export default function MonitorRegistry({ isHovered }: { isHovered: boolean }) {
Hetzner Node-01
-
-
SYS_STATUS:
-
- Online
-
-
+
+ SYS_STATUS: ONLINE
+
@@ -87,50 +57,80 @@ export default function MonitorRegistry({ isHovered }: { isHovered: boolean }) {
- {/* Hover View */}
-
-
-
- Service Registry
-
-
-
-
-
-
- {currentMonitors.map((m) => (
-
-
- {m.name}
-
-
-

-

-
-
- ))}
-
-
+ {/* REGISTRY VIEW: Only "Active" when hovered */}
+
+
+ Service Registry
+
+
+ {/* We pass isHovered here to start/stop the timer inside a separate lifecycle */}
+
- >
+
+ );
+}
+
+function RegistrySlider({ isHovered }: { isHovered: boolean }) {
+ const [page, setPage] = useState(0);
+ const totalPages = Math.ceil(MONITORS.length / 6);
+
+ useEffect(() => {
+ if (!isHovered) {
+ const timeout = setTimeout(() => setPage(0), 300);
+ return () => clearTimeout(timeout);
+ }
+
+ const interval = setInterval(() => {
+ setPage((p) => (p + 1) % totalPages);
+ }, 4000);
+
+ return () => clearInterval(interval);
+ }, [isHovered, totalPages]);
+
+ const currentItems = MONITORS.slice(page * 6, (page + 1) * 6);
+
+ return (
+
+
+
+ {currentItems.map((m) => (
+
+
+ {m.name}
+
+
+

+

+
+
+ ))}
+
+
+
);
}
diff --git a/components/PageLayout.tsx b/components/PageLayout.tsx
new file mode 100644
index 0000000..0f9651a
--- /dev/null
+++ b/components/PageLayout.tsx
@@ -0,0 +1,50 @@
+"use client";
+import { motion } from "framer-motion";
+import Link from "next/link";
+import { ArrowLeft } from "lucide-react";
+import Footer from "./Footer"; // Assuming you moved it to a component
+import { PageLayoutProps } from "@/types/index";
+
+export default function PageLayout({
+ children,
+ backLink,
+ backLabel = "BACK TO DASHBOARD",
+ maxWidth = "5xl",
+}: PageLayoutProps) {
+ const widthClass = {
+ "5xl": "max-w-5xl",
+ "6xl": "max-w-6xl",
+ "7xl": "max-w-7xl",
+ }[maxWidth];
+
+ return (
+
+
+
+ {backLink && (
+
+
+ {backLabel}
+
+ )}
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/data/projects.ts b/data/projects.ts
index c3e3548..4d1658f 100644
--- a/data/projects.ts
+++ b/data/projects.ts
@@ -1,4 +1,4 @@
-import { Project } from "@/types/project";
+import { Project } from "@/types/index";
export const PROJECT_REGISTRY: Project[] = [
{
diff --git a/types/index.ts b/types/index.ts
new file mode 100644
index 0000000..d557119
--- /dev/null
+++ b/types/index.ts
@@ -0,0 +1,37 @@
+import { ReactNode } from "react";
+
+export interface CategoryCardProps {
+ href: string;
+ icon: ReactNode;
+ title: string;
+ description: string;
+ tech: string[];
+ hoverColor: string;
+ activeTechColor: string;
+}
+
+export interface PageLayoutProps {
+ children: ReactNode;
+ backLink?: string;
+ backLabel?: string;
+ maxWidth?: "5xl" | "6xl" | "7xl";
+}
+
+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;
+}
diff --git a/types/project.ts b/types/project.ts
deleted file mode 100644
index ce00d04..0000000
--- a/types/project.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-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;
-}
\ No newline at end of file