Added prettier auto-format and cleaned up comments
This commit is contained in:
parent
49e62d5e2f
commit
1ff0d61a19
0
.prettierrc
Normal file
0
.prettierrc
Normal file
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
259
app/page.tsx
259
app/page.tsx
|
|
@ -1,10 +1,10 @@
|
||||||
"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, Activity } from "lucide-react";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import MonitorCard from '@/components/MonitorCard';
|
import MonitorCard from "@/components/MonitorCard";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [isHoveringMonitors, setIsHoveringMonitors] = useState(false);
|
const [isHoveringMonitors, setIsHoveringMonitors] = useState(false);
|
||||||
|
|
@ -12,83 +12,107 @@ export default function Home() {
|
||||||
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>
|
||||||
</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
|
||||||
|
|
||||||
{/* Right: Seniority Specs */}
|
|
||||||
<div className="flex-1 flex flex-col justify-around py-2 relative z-10">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Top Row Right: The Service Registry (Restored) */}
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ y: -5 }}
|
whileHover={{ y: -5 }}
|
||||||
onMouseEnter={() => setIsHoveringMonitors(true)}
|
onMouseEnter={() => setIsHoveringMonitors(true)}
|
||||||
onMouseLeave={() => setIsHoveringMonitors(false)}
|
onMouseLeave={() => setIsHoveringMonitors(false)}
|
||||||
|
|
@ -99,7 +123,7 @@ export default function Home() {
|
||||||
|
|
||||||
{/* 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">
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ y: -5 }}
|
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 transition-colors hover:border-blue-500/30"
|
className="p-6 rounded-3xl bg-neutral-900 border border-neutral-800 h-full flex flex-col justify-between min-h-[260px] relative transition-colors hover:border-blue-500/30"
|
||||||
>
|
>
|
||||||
|
|
@ -107,22 +131,28 @@ 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>
|
||||||
|
|
||||||
{/* Middle Row: Mobile Apps */}
|
{/* Middle Row: Mobile Apps */}
|
||||||
<Link href="/projects/mobile" className="group md:col-span-2">
|
<Link href="/projects/mobile" className="group md:col-span-2">
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ y: -5 }}
|
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 transition-colors hover:border-purple-500/30"
|
className="p-6 rounded-3xl bg-neutral-900 border border-neutral-800 h-full flex flex-col justify-between min-h-[260px] relative transition-colors hover:border-purple-500/30"
|
||||||
>
|
>
|
||||||
|
|
@ -130,22 +160,28 @@ 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>
|
||||||
|
|
||||||
{/* Middle Row: DevOps */}
|
{/* Middle Row: DevOps */}
|
||||||
<Link href="/projects/infrastructure" className="group md:col-span-2">
|
<Link href="/projects/infrastructure" className="group md:col-span-2">
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ y: -5 }}
|
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 transition-colors hover:border-green-500/30"
|
className="p-6 rounded-3xl bg-neutral-900 border border-neutral-800 h-full flex flex-col justify-between min-h-[260px] relative transition-colors hover:border-green-500/30"
|
||||||
>
|
>
|
||||||
|
|
@ -153,15 +189,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>
|
||||||
|
|
@ -173,33 +215,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="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>
|
|
||||||
</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>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
"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>
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* SYSTEM ARCHITECTURE (New Mermaid Section) */}
|
|
||||||
{project.mermaidChart && (
|
|
||||||
<section className="mb-8">
|
|
||||||
<div className="flex items-center gap-3 mb-8">
|
|
||||||
<h3 className="text-[10px] font-mono text-blue-500 uppercase tracking-[0.3em] font-bold">
|
|
||||||
TECHNICAL_ARCH // 01
|
|
||||||
</h3>
|
|
||||||
<div className="h-px flex-1 bg-neutral-900" />
|
|
||||||
<h2 className="text-xl font-bold tracking-tighter">System Architecture Log</h2>
|
|
||||||
</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 */}
|
|
||||||
{/* Updated Engineering Narrative Header */}
|
|
||||||
<article className="w-full py-12 border-t border-neutral-900 mt-16">
|
|
||||||
<div className="flex items-center gap-3 mb-12">
|
|
||||||
<h3 className="text-[10px] font-mono text-purple-500 uppercase tracking-[0.3em] font-bold">
|
|
||||||
PROJECT_LOG // 02
|
|
||||||
</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
|
|
||||||
prose-p:text-neutral-400 prose-p:leading-relaxed prose-p:text-[16px]
|
|
||||||
prose-h4:text-white prose-h4:text-sm prose-h4:mb-2 prose-h4:mt-8
|
|
||||||
prose-strong:text-white prose-strong:font-bold">
|
|
||||||
<ReactMarkdown>{project.engineeringStory}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -3,133 +3,175 @@
|
||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowLeft, ExternalLink, Github, ShieldCheck, Cpu, Users } from "lucide-react";
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
ExternalLink,
|
||||||
|
Github,
|
||||||
|
ShieldCheck,
|
||||||
|
Cpu,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
import { PROJECT_REGISTRY } from "@/data/projects";
|
import { PROJECT_REGISTRY } from "@/data/projects";
|
||||||
import Mermaid from "@/components/Mermaid";
|
import Mermaid from "@/components/Mermaid";
|
||||||
import ProjectShowcase from "@/components/ProjectShowcase";
|
import ProjectShowcase from "@/components/ProjectShowcase";
|
||||||
import ImageCarousel from "@/components/ImageCarousel";
|
import ImageCarousel from "@/components/ImageCarousel";
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
|
export default function ProjectDetail({
|
||||||
|
params,
|
||||||
export default function ProjectDetail({ params }: { params: Promise<{ category: string, slug: string }> }) {
|
}: {
|
||||||
|
params: Promise<{ category: string; slug: string }>;
|
||||||
|
}) {
|
||||||
const { category, slug } = use(params);
|
const { category, slug } = use(params);
|
||||||
const project = PROJECT_REGISTRY.find((p) => p.slug === slug);
|
const project = PROJECT_REGISTRY.find((p) => p.slug === slug);
|
||||||
|
|
||||||
if (!project) return <div>Project Not Found</div>;
|
if (!project) return <div>Project Not Found</div>;
|
||||||
|
|
||||||
if (!project) return <div className="p-24 text-white font-mono">Project Log Not Found.</div>;
|
if (!project)
|
||||||
|
return (
|
||||||
|
<div className="p-24 text-white font-mono">Project Log Not Found.</div>
|
||||||
|
);
|
||||||
|
|
||||||
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-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* 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">
|
<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}
|
<ArrowLeft size={12} /> Back to {category}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<header className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-20">
|
<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 }}>
|
<motion.div
|
||||||
<h1 className="text-6xl font-bold tracking-tighter mb-4">{project.title}</h1>
|
initial={{ opacity: 0, x: -20 }}
|
||||||
<p className="text-blue-500 font-mono text-sm uppercase tracking-widest mb-6">{project.subtitle}</p>
|
animate={{ opacity: 1, x: 0 }}
|
||||||
<p className="text-neutral-400 text-lg leading-relaxed">{project.description}</p>
|
>
|
||||||
|
<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">
|
<div className="flex gap-4 mt-8">
|
||||||
{project.liveUrl && (
|
{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">
|
<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} />
|
Launch Site <ExternalLink size={14} />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{project?.repoUrl && (
|
{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">
|
<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} />
|
View Source <Github size={14} />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Senior Stats Sidebar */}
|
{/* 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">
|
<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="space-y-8">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<ShieldCheck className="text-blue-500" />
|
<ShieldCheck className="text-blue-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">My Role</p>
|
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">
|
||||||
|
My Role
|
||||||
|
</p>
|
||||||
<p className="text-sm font-semibold">{project.role}</p>
|
<p className="text-sm font-semibold">{project.role}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Cpu className="text-purple-500" />
|
<Cpu className="text-purple-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">Stack</p>
|
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">
|
||||||
<p className="text-sm font-semibold">{project.stack.join(", ")}</p>
|
Stack
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-semibold">
|
||||||
|
{project.stack.join(", ")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Users className="text-green-500" />
|
<Users className="text-green-500" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">Impact</p>
|
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">
|
||||||
<p className="text-sm font-semibold">{project.metrics.join(" • ")}</p>
|
Impact
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-semibold">
|
||||||
|
{project.metrics.join(" • ")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
<section className="mb-20">
|
<section className="mb-20">
|
||||||
{/* Desktop Showcase View */}
|
{/* Desktop Showcase View */}
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<ProjectShowcase images={project.images} />
|
<ProjectShowcase images={project.images} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Carousel View */}
|
{/* Mobile Carousel View */}
|
||||||
<div className="block lg:hidden">
|
<div className="block lg:hidden">
|
||||||
<ImageCarousel images={project.images} />
|
<ImageCarousel images={project.images} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-4 text-center text-[10px] font-mono text-neutral-600 uppercase tracking-[0.2em]">
|
<p className="mt-4 text-center text-[10px] font-mono text-neutral-600 uppercase tracking-[0.2em]">
|
||||||
Interactive Gallery — Select or swipe to explore
|
Interactive Gallery — Select or swipe to explore
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* SYSTEM ARCHITECTURE (New Mermaid Section) */}
|
{/* Mermaid */}
|
||||||
{project.mermaidChart && (
|
{project.mermaidChart && (
|
||||||
<section className="mb-16">
|
<section className="mb-16">
|
||||||
<div className="flex items-center gap-3 mb-8">
|
<div className="flex items-center gap-3 mb-8">
|
||||||
<div className="h-px flex-1 bg-neutral-900" />
|
<div className="h-px flex-1 bg-neutral-900" />
|
||||||
<h3 className="text-[10px] font-mono text-neutral-500 uppercase tracking-[0.3em]">
|
<h3 className="text-[10px] font-mono text-neutral-500 uppercase tracking-[0.3em]">
|
||||||
System Architecture Log
|
System Architecture Log
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-px flex-1 bg-neutral-900" />
|
<div className="h-px flex-1 bg-neutral-900" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-neutral-900/20 rounded-3xl border border-neutral-800/50 p-4 md:p-12">
|
|
||||||
<Mermaid chart={project.mermaidChart} />
|
|
||||||
|
|
||||||
|
<div className="bg-neutral-900/20 rounded-3xl border border-neutral-800/50 p-4 md:p-12">
|
||||||
|
<Mermaid chart={project.mermaidChart} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Engineering Narrative */}
|
{/* Engineering Narrative */}
|
||||||
{/* Updated Engineering Narrative Header */}
|
<section className="w-full pb-20 mt-12">
|
||||||
<section className="w-full pb-20 mt-12">
|
<div className="flex items-center gap-3 mb-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">
|
||||||
<h3 className="text-[10px] font-mono text-blue-500 uppercase tracking-[0.3em] font-bold">
|
PROJECT LOG // {project.storyLabel || "NARRATIVE"}
|
||||||
PROJECT LOG // {project.storyLabel || "NARRATIVE"}
|
</h3>
|
||||||
</h3>
|
<div className="h-px flex-1 bg-neutral-900" />
|
||||||
<div className="h-px flex-1 bg-neutral-900" />
|
<h2 className="text-xl font-bold tracking-tighter">
|
||||||
<h2 className="text-xl font-bold tracking-tighter">The Engineering Story</h2>
|
The Engineering Story
|
||||||
</div>
|
</h2>
|
||||||
|
</div>
|
||||||
<div className="prose prose-invert prose-neutral max-w-none text-left">
|
|
||||||
<ReactMarkdown>{project.engineeringStory}</ReactMarkdown>
|
<div className="prose prose-invert prose-neutral max-w-none text-left">
|
||||||
</div>
|
<ReactMarkdown>{project.engineeringStory}</ReactMarkdown>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -9,49 +9,58 @@ 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.",
|
||||||
},
|
},
|
||||||
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.",
|
||||||
},
|
},
|
||||||
infrastructure: {
|
infrastructure: {
|
||||||
title: "DevOps & Infrastructure",
|
title: "DevOps & Infrastructure",
|
||||||
icon: <Server className="w-8 h-8 text-green-400" />,
|
icon: <Server className="w-8 h-8 text-green-400" />,
|
||||||
description: "Self-hosted systems architecture and automated deployment pipelines.",
|
description:
|
||||||
}
|
"Self-hosted systems architecture and automated deployment pipelines.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
// 1. Get metadata for the header
|
|
||||||
const meta = CATEGORY_META[category as keyof typeof CATEGORY_META];
|
const meta = CATEGORY_META[category as keyof typeof CATEGORY_META];
|
||||||
|
|
||||||
// 2. Filter the registry to find projects belonging to this category
|
const filteredProjects = PROJECT_REGISTRY.filter(
|
||||||
const filteredProjects = PROJECT_REGISTRY.filter(p => p.category === category);
|
(p) => p.category === category,
|
||||||
|
);
|
||||||
|
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
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>
|
||||||
<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"
|
||||||
|
>
|
||||||
<ArrowLeft size={14} /> BACK TO DASHBOARD
|
<ArrowLeft size={14} /> BACK TO DASHBOARD
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="max-w-5xl mx-auto"
|
className="max-w-5xl mx-auto"
|
||||||
|
|
@ -60,44 +69,51 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
|
||||||
{meta.icon}
|
{meta.icon}
|
||||||
<h1 className="text-5xl font-bold">{meta.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">{meta.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">
|
||||||
{filteredProjects.map((project, index) => (
|
{filteredProjects.map((project, index) => (
|
||||||
<Link
|
<Link
|
||||||
key={project.slug}
|
key={project.slug}
|
||||||
href={`/projects/${category}/${project.slug}`}>
|
href={`/projects/${category}/${project.slug}`}
|
||||||
<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">
|
<motion.div
|
||||||
<div>
|
layout
|
||||||
<h3 className="text-2xl font-semibold mb-2">{project.title}</h3>
|
initial={{ opacity: 0, x: -20 }}
|
||||||
<p className="text-neutral-500">{project.description}</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 => (
|
|
||||||
<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>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,19 @@ export default function ImageCarousel({ images }: GalleryProps) {
|
||||||
|
|
||||||
const imageIndex = Math.abs(page % images.length);
|
const imageIndex = Math.abs(page % images.length);
|
||||||
|
|
||||||
const paginate = useCallback((newDirection: number) => {
|
const paginate = useCallback(
|
||||||
setPage([page + newDirection, newDirection]);
|
(newDirection: number) => {
|
||||||
}, [page]);
|
setPage([page + newDirection, newDirection]);
|
||||||
|
},
|
||||||
|
[page],
|
||||||
|
);
|
||||||
|
|
||||||
// AUTO-PLAY LOGIC
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAutoPlaying) return;
|
if (!isAutoPlaying) return;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
paginate(1);
|
paginate(1);
|
||||||
}, 5000); // 5 seconds is the "sweet spot" for technical analysis
|
}, 5000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [paginate, isAutoPlaying]);
|
}, [paginate, isAutoPlaying]);
|
||||||
|
|
@ -43,10 +45,10 @@ export default function ImageCarousel({ images }: GalleryProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative aspect-video w-full overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900"
|
className="relative aspect-video w-full overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900"
|
||||||
onMouseEnter={() => setIsAutoPlaying(false)} // Pause on hover
|
onMouseEnter={() => setIsAutoPlaying(false)}
|
||||||
onMouseLeave={() => setIsAutoPlaying(true)} // Resume when mouse leaves
|
onMouseLeave={() => setIsAutoPlaying(true)}
|
||||||
>
|
>
|
||||||
<AnimatePresence initial={false} custom={direction}>
|
<AnimatePresence initial={false} custom={direction}>
|
||||||
<motion.img
|
<motion.img
|
||||||
|
|
@ -64,7 +66,7 @@ export default function ImageCarousel({ images }: GalleryProps) {
|
||||||
drag="x"
|
drag="x"
|
||||||
dragConstraints={{ left: 0, right: 0 }}
|
dragConstraints={{ left: 0, right: 0 }}
|
||||||
dragElastic={1}
|
dragElastic={1}
|
||||||
onDragStart={() => setIsAutoPlaying(false)} // Kill auto-play on interaction
|
onDragStart={() => setIsAutoPlaying(false)}
|
||||||
onDragEnd={(e, { offset }) => {
|
onDragEnd={(e, { offset }) => {
|
||||||
const swipe = Math.abs(offset.x) > 50;
|
const swipe = Math.abs(offset.x) > 50;
|
||||||
if (swipe) paginate(offset.x > 0 ? -1 : 1);
|
if (swipe) paginate(offset.x > 0 ? -1 : 1);
|
||||||
|
|
@ -78,7 +80,7 @@ export default function ImageCarousel({ images }: GalleryProps) {
|
||||||
<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"
|
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={() => {
|
onClick={() => {
|
||||||
setIsAutoPlaying(false); // Kill auto-play
|
setIsAutoPlaying(false);
|
||||||
paginate(-1);
|
paginate(-1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -87,7 +89,7 @@ export default function ImageCarousel({ images }: GalleryProps) {
|
||||||
<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"
|
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={() => {
|
onClick={() => {
|
||||||
setIsAutoPlaying(false); // Kill auto-play
|
setIsAutoPlaying(false);
|
||||||
paginate(1);
|
paginate(1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -95,9 +97,9 @@ export default function ImageCarousel({ images }: GalleryProps) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar (Visual Timer) */}
|
{/* Progress Bar */}
|
||||||
{isAutoPlaying && (
|
{isAutoPlaying && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={imageIndex}
|
key={imageIndex}
|
||||||
initial={{ width: "0%" }}
|
initial={{ width: "0%" }}
|
||||||
animate={{ width: "100%" }}
|
animate={{ width: "100%" }}
|
||||||
|
|
@ -107,4 +109,4 @@ export default function ImageCarousel({ images }: GalleryProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ export default function Mermaid({ chart }: { chart: string }) {
|
||||||
});
|
});
|
||||||
mermaid.contentLoaded();
|
mermaid.contentLoaded();
|
||||||
|
|
||||||
// Check if the rendered diagram is taller than 400px
|
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
const height = contentRef.current.scrollHeight;
|
const height = contentRef.current.scrollHeight;
|
||||||
setNeedsExpansion(height > 400);
|
setNeedsExpansion(height > 400);
|
||||||
|
|
@ -36,21 +35,25 @@ export default function Mermaid({ chart }: { chart: string }) {
|
||||||
>
|
>
|
||||||
{/* Legend */}
|
{/* 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="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="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
<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>
|
<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>
|
||||||
<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">
|
<div ref={contentRef} className="mermaid flex justify-center">
|
||||||
{chart}
|
{chart}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The "Fade to Darkness" Overlay - only show if needs expansion */}
|
{/* The "Fade to Darkness" Overlay (when expansion is needed) */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{needsExpansion && !isExpanded && (
|
{needsExpansion && !isExpanded && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -63,7 +66,7 @@ export default function Mermaid({ chart }: { chart: string }) {
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Expand/Collapse Button - only show if needs expansion */}
|
{/* Expand/Collapse Button (when expansion is needed) */}
|
||||||
{needsExpansion && (
|
{needsExpansion && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
@ -71,18 +74,25 @@ export default function Mermaid({ chart }: { chart: string }) {
|
||||||
setIsExpanded(!isExpanded);
|
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
|
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"
|
isExpanded
|
||||||
: "bg-neutral-900 border-neutral-800 text-neutral-500 group-hover:bg-neutral-800 group-hover:border-neutral-600 group-hover:text-white"
|
? "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 ? (
|
{isExpanded ? (
|
||||||
<> <Minimize2 size={12} /> Collapse Logic </>
|
<>
|
||||||
|
{" "}
|
||||||
|
<Minimize2 size={12} /> Collapse Logic{" "}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<> <Maximize2 size={12} /> Expand Architecture </>
|
<>
|
||||||
|
{" "}
|
||||||
|
<Maximize2 size={12} /> Expand Architecture{" "}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Activity } from 'lucide-react';
|
import { Activity } from "lucide-react";
|
||||||
|
|
||||||
const MONITORS = [
|
const MONITORS = [
|
||||||
{ id: 2, name: "Datasaur" }, { id: 6, name: "Audiobookshelf" },
|
{ id: 2, name: "Datasaur" },
|
||||||
{ id: 7, name: "Woodpecker CI" }, { id: 8, name: "Forgejo Git" },
|
{ id: 6, name: "Audiobookshelf" },
|
||||||
{ id: 9, name: "Server dashboard" }, { id: 10, name: "Ratoong" },
|
{ id: 7, name: "Woodpecker CI" },
|
||||||
{ id: 3, name: "Dozzle" }, { id: 12, name: "Observatory" },
|
{ id: 8, name: "Forgejo Git" },
|
||||||
{ id: 13, name: "Surf hub" }, { id: 11, name: "Anime list" },
|
{ id: 9, name: "Server dashboard" },
|
||||||
{ id: 5, name: "Wiki" }, { id: 4, name: "Watchtower" },
|
{ 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;
|
const ITEMS_PER_PAGE = 6;
|
||||||
|
|
@ -19,99 +25,111 @@ export default function MonitorRegistry({ isHovered }: { isHovered: boolean }) {
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const totalPages = Math.ceil(MONITORS.length / ITEMS_PER_PAGE);
|
const totalPages = Math.ceil(MONITORS.length / ITEMS_PER_PAGE);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: NodeJS.Timeout | null = null;
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
if (isHovered && totalPages > 1) {
|
if (isHovered && totalPages > 1) {
|
||||||
// Only start the interval if we are hovered
|
interval = setInterval(() => {
|
||||||
interval = setInterval(() => {
|
setPage((prev) => (prev + 1) % totalPages);
|
||||||
setPage((prev) => (prev + 1) % totalPages);
|
}, 4000);
|
||||||
}, 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 () => {
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
if (!isHovered) {
|
||||||
|
setPage(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isHovered, totalPages]);
|
||||||
|
|
||||||
|
const currentMonitors = MONITORS.slice(
|
||||||
|
page * ITEMS_PER_PAGE,
|
||||||
|
(page + 1) * ITEMS_PER_PAGE,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Default View */}
|
{/* 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">
|
<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 */}
|
{/* Header Section */}
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<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)]" />
|
<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>
|
<span className="font-medium text-white tracking-tight">
|
||||||
</div>
|
Hetzner Node-01
|
||||||
<div className="flex gap-2 items-center">
|
</span>
|
||||||
<p className="text-xs text-neutral-500 font-mono">SYS_STATUS:</p>
|
</div>
|
||||||
<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 className="flex gap-2 items-center">
|
||||||
</div>
|
<p className="text-xs text-neutral-500 font-mono">SYS_STATUS:</p>
|
||||||
</div>
|
<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">
|
||||||
<Activity className="text-neutral-800 w-10 h-10 -mt-1" />
|
Online
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
{/* 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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<Activity className="text-neutral-800 w-10 h-10 -mt-1" />
|
||||||
</motion.div>
|
</div>
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
<div className="mt-8 grid grid-cols-2 gap-4 border-t border-neutral-800/50 pt-6">
|
||||||
</div>
|
<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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default function ProjectShowcase({ images }: { images: string[] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 h-[500px]">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 h-[500px]">
|
||||||
{/* Large Featured Image (Left 9 Columns) */}
|
{/* Main Image */}
|
||||||
<div className="lg:col-span-9 relative overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900 group">
|
<div className="lg:col-span-9 relative overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900 group">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.img
|
<motion.img
|
||||||
|
|
@ -21,7 +21,7 @@ export default function ProjectShowcase({ images }: { images: string[] }) {
|
||||||
className="absolute inset-0 h-full w-full object-cover"
|
className="absolute inset-0 h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Subtle Overlay Label */}
|
{/* 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">
|
<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">
|
<p className="text-[10px] font-mono uppercase tracking-widest text-white/70">
|
||||||
|
|
@ -30,21 +30,25 @@ export default function ProjectShowcase({ images }: { images: string[] }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Thumbnail Column (Right 3 Columns) */}
|
{/* 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">
|
<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) => (
|
{images.map((img, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setIndex(i)}
|
onClick={() => setIndex(i)}
|
||||||
className={`relative flex-shrink-0 w-24 lg:w-full aspect-video rounded-xl border-2 transition-all overflow-hidden ${
|
className={`relative flex-shrink-0 w-24 lg:w-full aspect-video rounded-xl border-2 transition-all overflow-hidden ${
|
||||||
i === index
|
i === index
|
||||||
? "border-blue-500 ring-4 ring-blue-500/10"
|
? "border-blue-500 ring-4 ring-blue-500/10"
|
||||||
: "border-neutral-800 opacity-40 hover:opacity-100"
|
: "border-neutral-800 opacity-40 hover:opacity-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img src={img} className="h-full w-full object-cover" alt={`Thumb ${i}`} />
|
<img
|
||||||
|
src={img}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
alt={`Thumb ${i}`}
|
||||||
|
/>
|
||||||
{i === index && (
|
{i === index && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId="active-thumb"
|
layoutId="active-thumb"
|
||||||
className="absolute inset-0 bg-blue-500/10 z-10"
|
className="absolute inset-0 bg-blue-500/10 z-10"
|
||||||
/>
|
/>
|
||||||
|
|
@ -54,4 +58,4 @@ export default function ProjectShowcase({ images }: { images: string[] }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -25,6 +25,7 @@
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.4",
|
"eslint-config-next": "16.1.4",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
|
@ -4324,6 +4325,22 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-config-prettier": {
|
||||||
|
"version": "10.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
||||||
|
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint-config-prettier"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-import-resolver-node": {
|
"node_modules/eslint-import-resolver-node": {
|
||||||
"version": "0.3.9",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.4",
|
"eslint-config-next": "16.1.4",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue