diff --git a/app/infrastructure/page.tsx b/app/infrastructure/page.tsx index ee0c4e9..c856077 100644 --- a/app/infrastructure/page.tsx +++ b/app/infrastructure/page.tsx @@ -1,7 +1,9 @@ "use client"; + import { motion } from "framer-motion"; -import { ShieldCheck, Zap, Globe, Cpu, Terminal } from "lucide-react"; +import { ShieldCheck, Zap, Cpu, Terminal } from "lucide-react"; import Link from "next/link"; +import PageLayout from "@/components/PageLayout"; const CAPABILITIES = [ { @@ -41,13 +43,13 @@ const CAPABILITIES = [ export default function InfrastructurePage() { return ( -
- {/* Tightened Header */} -
+ + {/* Header Section */} +

- Infrastructure and Operationss + Infrastructure and Operations

@@ -57,14 +59,15 @@ export default function InfrastructurePage() {

- {/* Tightened Specification List */} -
+ {/* 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

-

+
-
-
-
-

Pipeline Status

- Build Status -
-
-

Engine: Next.js 15 (Standalone)

-
- -
-
-

- Deploy: {process.env.NEXT_PUBLIC_APP_VERSION || "v1.0.0-dev"} -

-
-
-
- + + + ); +} + +function TechnicalFocus({ + label, + color, + text, +}: { + label: string; + color: string; + text: string; +}) { + return ( +
+

+ {label} +

+

{text}

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

Pipeline Status

+ Build Status +
+
+

Engine: Next.js 15 (Standalone)

+
+ +
+
+

+ Deploy: {process.env.NEXT_PUBLIC_APP_VERSION || "v1.0.0-dev"} +

+
+
+ ); +} 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 */} -
-
-
- - Traffic Flow - -
-
-
- - Service Node - -
-
- - {/* Chart Area */} -
-
{chart}
-
- - {/* 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 */} -
-
-
- - Traffic Flow - -
-
-
- - Service Node - -
-
- -
- {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" - > -
- - App Screenshot - -
-
- ); - })} -
- - {/* 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} - -
- up - ms -
-
- ))} -
-
+ {/* 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} + +
+ up + ms +
+
+ ))} +
+
+
); } 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