Added project details pages. Cleaned up dashboard
This commit is contained in:
parent
b0f5d62e3e
commit
5d0a86645d
60
app/page.tsx
60
app/page.tsx
|
|
@ -3,8 +3,12 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Globe, Smartphone, Server, Gamepad2, Activity } from 'lucide-react';
|
import { Globe, Smartphone, Server, Gamepad2, Activity } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import MonitorCard from '@/components/MonitorCard';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [isHoveringMonitors, setIsHoveringMonitors] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
|
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
|
|
@ -19,9 +23,6 @@ export default function Home() {
|
||||||
<a href="https://linkedin.com/in/georgew" className="text-neutral-500 hover:text-white transition-colors">
|
<a href="https://linkedin.com/in/georgew" className="text-neutral-500 hover:text-white transition-colors">
|
||||||
LinkedIn
|
LinkedIn
|
||||||
</a>
|
</a>
|
||||||
{/* <a href="https://github.com/georgew" className="text-neutral-500 hover:text-white transition-colors">
|
|
||||||
GitHub
|
|
||||||
</a> */}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -89,58 +90,11 @@ export default function Home() {
|
||||||
{/* Top Row Right: The Service Registry (Restored) */}
|
{/* Top Row Right: The Service Registry (Restored) */}
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ y: -5 }}
|
whileHover={{ y: -5 }}
|
||||||
|
onMouseEnter={() => setIsHoveringMonitors(true)}
|
||||||
|
onMouseLeave={() => setIsHoveringMonitors(false)}
|
||||||
className="group md:col-span-2 p-6 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-center relative overflow-hidden transition-all duration-300 min-h-[180px]"
|
className="group md:col-span-2 p-6 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-center relative overflow-hidden transition-all duration-300 min-h-[180px]"
|
||||||
>
|
>
|
||||||
{/* The Monitor Map (Easily editable) */}
|
<MonitorCard isHovered={isHoveringMonitors} />
|
||||||
{(() => {
|
|
||||||
const monitors = [
|
|
||||||
{ id: 2, name: "Datasaur" },
|
|
||||||
{ id: 6, name: "Audiobookshelf" },
|
|
||||||
{ id: 7, name: "Woodpecker CI" },
|
|
||||||
{ id: 8, name: "Forgejo Git" },
|
|
||||||
{ id: 9, name: "Server dashboard" },
|
|
||||||
{ id: 10, name: "Ratoong" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Default View */}
|
|
||||||
<div className="flex min-h-[250px] items-center justify-between w-full group-hover:opacity-0 group-hover:pointer-events-none transition-opacity duration-300">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
|
||||||
<span className="font-medium text-white">Hetzner Node-01</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<p className="text-sm text-neutral-500">System Status:</p>
|
|
||||||
<img
|
|
||||||
src="https://status.georgew.dev/api/status-page/dashboard/badge"
|
|
||||||
alt="Overall Status"
|
|
||||||
className="h-5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Activity className="text-neutral-700 w-8 h-8" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hover View: Friendly Names */}
|
|
||||||
<div className="absolute inset-0 p-6 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-center bg-neutral-900/95 backdrop-blur-sm">
|
|
||||||
<h4 className="text-[12px] font-mono text-neutral-500 mb-3 uppercase tracking-[0.2em]">Service Registry</h4>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-1 gap-2">
|
|
||||||
{monitors.map((m) => (
|
|
||||||
<div key={m.id} className="flex items-center justify-between bg-neutral-800/40 p-2 rounded-lg border border-neutral-700/30">
|
|
||||||
<span className="text-[11px] font-medium text-neutral-300 truncate mr-2">{m.name}</span>
|
|
||||||
<div className="flex gap-1 shrink-0">
|
|
||||||
<img src={`https://status.georgew.dev/api/badge/${m.id}/status`} className="h-4" alt="up" />
|
|
||||||
<img src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`} className="h-4 opacity-60" alt="ms" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Middle Row: Web Systems */}
|
{/* Middle Row: Web Systems */}
|
||||||
|
|
|
||||||
126
app/projects/[category]/[slug]/page.tsx
Normal file
126
app/projects/[category]/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { use } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowLeft, ExternalLink, Github, ShieldCheck, Cpu, Users } from "lucide-react";
|
||||||
|
|
||||||
|
// This would eventually move to a separate data file
|
||||||
|
const PROJECT_DETAILS = {
|
||||||
|
ratoong: {
|
||||||
|
title: "Ratoong",
|
||||||
|
subtitle: "Regulatory-Compliant Data Platform",
|
||||||
|
role: "Lead Full-Stack Engineer",
|
||||||
|
duration: "2022 — 2024",
|
||||||
|
stack: ["Node.js", "PostgreSQL", "React", "Docker"],
|
||||||
|
metrics: ["99.9% Uptime", "Zero-Data-Loss Integrity", "ISO 27001 Ready"],
|
||||||
|
description: "Architected a high-integrity platform designed to meet rigid regulatory requirements. Focused on immutable audit trails and secure data orchestration.",
|
||||||
|
images: ["/ratoong-1.jpg", "/ratoong-2.jpg"], // Place these in /public
|
||||||
|
liveUrl: "https://ratoong.com",
|
||||||
|
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||||
|
},
|
||||||
|
datasaur: {
|
||||||
|
title: "Datasaur",
|
||||||
|
subtitle: "Personal R&D Pipeline",
|
||||||
|
role: "Architect & Creator",
|
||||||
|
duration: "2025 — Present",
|
||||||
|
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
||||||
|
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
||||||
|
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
||||||
|
images: ["/datasaur-1.jpg"],
|
||||||
|
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||||
|
liveUrl: "https://ratoong.com",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProjectDetail({ params }: { params: Promise<{ category: string, slug: string }> }) {
|
||||||
|
const { category, slug } = use(params);
|
||||||
|
const project = PROJECT_DETAILS[slug as keyof typeof PROJECT_DETAILS];
|
||||||
|
|
||||||
|
if (!project) return <div className="p-24 text-white font-mono">Project Log Not Found.</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<Link href={`/projects/${category}`} className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-[10px] uppercase tracking-widest">
|
||||||
|
<ArrowLeft size={12} /> Back to {category}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Header Section */}
|
||||||
|
<header className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-20">
|
||||||
|
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
|
||||||
|
<h1 className="text-6xl font-bold tracking-tighter mb-4">{project.title}</h1>
|
||||||
|
<p className="text-blue-500 font-mono text-sm uppercase tracking-widest mb-6">{project.subtitle}</p>
|
||||||
|
<p className="text-neutral-400 text-lg leading-relaxed">{project.description}</p>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mt-8">
|
||||||
|
{project.liveUrl && (
|
||||||
|
<a href={project.liveUrl} className="flex items-center gap-2 bg-white text-black px-6 py-3 rounded-full font-bold text-sm hover:bg-neutral-200 transition-all">
|
||||||
|
Launch Site <ExternalLink size={14} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{project?.repoUrl && (
|
||||||
|
<a href={project.repoUrl} className="flex items-center gap-2 border border-neutral-800 px-6 py-3 rounded-full font-bold text-sm hover:bg-neutral-900 transition-all">
|
||||||
|
View Source <Github size={14} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Senior Stats Sidebar */}
|
||||||
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="bg-neutral-900/30 border border-neutral-800 p-8 rounded-3xl h-fit">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<ShieldCheck className="text-blue-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">My Role</p>
|
||||||
|
<p className="text-sm font-semibold">{project.role}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Cpu className="text-purple-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">Stack</p>
|
||||||
|
<p className="text-sm font-semibold">{project.stack.join(", ")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Users className="text-green-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-500 uppercase font-mono tracking-tighter">Impact</p>
|
||||||
|
<p className="text-sm font-semibold">{project.metrics.join(" • ")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Image Gallery */}
|
||||||
|
<section className="mb-20">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{project.images.map((img, i) => (
|
||||||
|
<motion.div key={i} whileHover={{ scale: 1.01 }} className="aspect-video bg-neutral-900 rounded-2xl border border-neutral-800 overflow-hidden relative">
|
||||||
|
{/* Placeholder for your actual images */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center text-neutral-700 font-mono text-xs italic">
|
||||||
|
[Screenshot: {project.title} - Layer {i + 1}]
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Engineering Narrative */}
|
||||||
|
<article className="max-w-3xl border-t border-neutral-900 pt-16">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 italic underline decoration-blue-500 underline-offset-8">The Engineering Story</h2>
|
||||||
|
<div className="prose prose-invert prose-neutral max-w-none text-neutral-400 leading-loose">
|
||||||
|
{/* Content drafted below */}
|
||||||
|
<p>Detail how you approached the architecture, the specific trade-offs you made, and how you ensured regulatory compliance in the data layer.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
app/projects/[category]/page copy.tsx
Normal file
120
app/projects/[category]/page copy.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
"use client";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Globe, Smartphone, ArrowLeft } from "lucide-react";
|
||||||
|
import { use } from "react";
|
||||||
|
|
||||||
|
const categories = {
|
||||||
|
web: {
|
||||||
|
title: "Web Systems",
|
||||||
|
icon: <Globe className="w-8 h-8 text-blue-400" />,
|
||||||
|
description: "Architecting scalable web applications and distributed systems.",
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "Ratoong",
|
||||||
|
detail: "Professional production platform.",
|
||||||
|
stack: ["Node.js", "PostgreSQL", "Caddy"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Datasaur",
|
||||||
|
detail: "Full-stack data science pipeline.",
|
||||||
|
stack: ["Python", "FastAPI", "Next.js"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
title: "Mobile Apps",
|
||||||
|
icon: <Smartphone className="w-8 h-8 text-purple-400" />,
|
||||||
|
description: "Building cross-platform experiences with Flutter and native integrations.",
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "Flutter App 1",
|
||||||
|
detail: "Active Development - Coming Soon",
|
||||||
|
stack: ["Flutter", "Dart", "Firebase"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Flutter App 2",
|
||||||
|
detail: "Internal R&D Prototype",
|
||||||
|
stack: ["Flutter", "Riverpod", "SQLite"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CategoryPage({ params }: { params: Promise<{ category: string }> }) {
|
||||||
|
|
||||||
|
|
||||||
|
const resolvedParams = use(params);
|
||||||
|
const category = resolvedParams.category;
|
||||||
|
|
||||||
|
const data = categories[category as keyof typeof categories];
|
||||||
|
|
||||||
|
if (!data) return <div className="p-24 text-white">Category not found.</div>;
|
||||||
|
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="p-24 text-white font-mono">
|
||||||
|
<h1 className="text-2xl mb-4">404: Category Not Found</h1>
|
||||||
|
<p className="text-neutral-500">Path: /projects/{category}</p>
|
||||||
|
<Link href="/" className="text-blue-400 underline mt-4 block">Return Home</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-[#0a0a0a] text-white p-8 md:p-24">
|
||||||
|
<Link href="/" className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-xs">
|
||||||
|
<ArrowLeft size={14} /> BACK TO DASHBOARD
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="max-w-5xl mx-auto"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
{data.icon}
|
||||||
|
<h1 className="text-5xl font-bold">{data.title}</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-xl text-neutral-400 max-w-2xl mb-16">{data.description}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-8">
|
||||||
|
{data.projects.map((project, index) => (
|
||||||
|
<Link
|
||||||
|
key={project.name}
|
||||||
|
href={`/projects/${category}/${project.name.toLowerCase()}`}>
|
||||||
|
<motion.div
|
||||||
|
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{
|
||||||
|
delay: index * 0.1,
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
className="group p-8 rounded-3xl bg-neutral-900/50 border border-neutral-800 hover:border-neutral-700 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-semibold mb-2">{project.name}</h3>
|
||||||
|
<p className="text-neutral-500">{project.detail}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{project.stack.map(s => (
|
||||||
|
<span key={s} className="text-[10px] font-mono bg-neutral-800 text-neutral-400 px-3 py-1 rounded-full uppercase">
|
||||||
|
{s}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,43 +1,25 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Globe, Smartphone, ArrowLeft } from "lucide-react";
|
import { Globe, Smartphone, ArrowLeft, Server } from "lucide-react";
|
||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
|
import { PROJECT_REGISTRY } from "@/data/projects";
|
||||||
|
|
||||||
const categories = {
|
const CATEGORY_META = {
|
||||||
web: {
|
web: {
|
||||||
title: "Web Systems",
|
title: "Web Systems",
|
||||||
icon: <Globe className="w-8 h-8 text-blue-400" />,
|
icon: <Globe className="w-8 h-8 text-blue-400" />,
|
||||||
description: "Architecting scalable web applications and distributed systems.",
|
description: "Architecting scalable web applications and distributed systems.",
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: "Ratoong",
|
|
||||||
detail: "Professional production platform.",
|
|
||||||
stack: ["Node.js", "PostgreSQL", "Caddy"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Datasaur",
|
|
||||||
detail: "Full-stack data science pipeline.",
|
|
||||||
stack: ["Python", "FastAPI", "Next.js"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
mobile: {
|
mobile: {
|
||||||
title: "Mobile Apps",
|
title: "Mobile Apps",
|
||||||
icon: <Smartphone className="w-8 h-8 text-purple-400" />,
|
icon: <Smartphone className="w-8 h-8 text-purple-400" />,
|
||||||
description: "Building cross-platform experiences with Flutter and native integrations.",
|
description: "Building cross-platform experiences with Flutter and native integrations.",
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: "Flutter App 1",
|
|
||||||
detail: "Active Development - Coming Soon",
|
|
||||||
stack: ["Flutter", "Dart", "Firebase"]
|
|
||||||
},
|
},
|
||||||
{
|
infrastructure: {
|
||||||
name: "Flutter App 2",
|
title: "DevOps & Infrastructure",
|
||||||
detail: "Internal R&D Prototype",
|
icon: <Server className="w-8 h-8 text-green-400" />,
|
||||||
stack: ["Flutter", "Riverpod", "SQLite"]
|
description: "Self-hosted systems architecture and automated deployment pipelines.",
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,21 +29,22 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
|
||||||
const resolvedParams = use(params);
|
const resolvedParams = use(params);
|
||||||
const category = resolvedParams.category;
|
const category = resolvedParams.category;
|
||||||
|
|
||||||
const data = categories[category as keyof typeof categories];
|
// 1. Get metadata for the header
|
||||||
|
const meta = CATEGORY_META[category as keyof typeof CATEGORY_META];
|
||||||
|
|
||||||
if (!data) return <div className="p-24 text-white">Category not found.</div>;
|
// 2. Filter the registry to find projects belonging to this category
|
||||||
|
const filteredProjects = PROJECT_REGISTRY.filter(p => p.category === category);
|
||||||
|
|
||||||
|
if (!meta) {
|
||||||
if (!data) {
|
|
||||||
return (
|
return (
|
||||||
<div className="p-24 text-white font-mono">
|
<div className="p-24 text-white font-mono">
|
||||||
<h1 className="text-2xl mb-4">404: Category Not Found</h1>
|
<h1 className="text-2xl mb-4">404: Category Not Found</h1>
|
||||||
<p className="text-neutral-500">Path: /projects/{category}</p>
|
|
||||||
<Link href="/" className="text-blue-400 underline mt-4 block">Return Home</Link>
|
<Link href="/" className="text-blue-400 underline mt-4 block">Return Home</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-[#0a0a0a] text-white p-8 md:p-24">
|
<main className="min-h-screen bg-[#0a0a0a] text-white p-8 md:p-24">
|
||||||
<Link href="/" className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-xs">
|
<Link href="/" className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-xs">
|
||||||
|
|
@ -74,15 +57,18 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
|
||||||
className="max-w-5xl mx-auto"
|
className="max-w-5xl mx-auto"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
{data.icon}
|
{meta.icon}
|
||||||
<h1 className="text-5xl font-bold">{data.title}</h1>
|
<h1 className="text-5xl font-bold">{meta.title}</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xl text-neutral-400 max-w-2xl mb-16">{data.description}</p>
|
<p className="text-xl text-neutral-400 max-w-2xl mb-16">{meta.description}</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8">
|
<div className="grid grid-cols-1 gap-8">
|
||||||
{data.projects.map((project, index) => (
|
{filteredProjects.map((project, index) => (
|
||||||
|
<Link
|
||||||
|
key={project.slug}
|
||||||
|
href={`/projects/${category}/${project.slug}`}>
|
||||||
<motion.div
|
<motion.div
|
||||||
key={project.name}
|
|
||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
|
@ -96,8 +82,8 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
|
||||||
>
|
>
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-semibold mb-2">{project.name}</h3>
|
<h3 className="text-2xl font-semibold mb-2">{project.title}</h3>
|
||||||
<p className="text-neutral-500">{project.detail}</p>
|
<p className="text-neutral-500">{project.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{project.stack.map(s => (
|
{project.stack.map(s => (
|
||||||
|
|
@ -108,6 +94,7 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
117
components/MonitorCard.tsx
Normal file
117
components/MonitorCard.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Activity } from 'lucide-react';
|
||||||
|
|
||||||
|
const MONITORS = [
|
||||||
|
{ id: 2, name: "Datasaur" }, { id: 6, name: "Audiobookshelf" },
|
||||||
|
{ id: 7, name: "Woodpecker CI" }, { id: 8, name: "Forgejo Git" },
|
||||||
|
{ id: 9, name: "Server dashboard" }, { id: 10, name: "Ratoong" },
|
||||||
|
{ id: 3, name: "Dozzle" }, { id: 12, name: "Observatory" },
|
||||||
|
{ id: 13, name: "Surf hub" }, { id: 11, name: "Anime list" },
|
||||||
|
{ id: 5, name: "Wiki" }, { id: 4, name: "Watchtower" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 6;
|
||||||
|
|
||||||
|
export default function MonitorRegistry({ isHovered }: { isHovered: boolean }) {
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const totalPages = Math.ceil(MONITORS.length / ITEMS_PER_PAGE);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
if (isHovered && totalPages > 1) {
|
||||||
|
// Only start the interval if we are hovered
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setPage((prev) => (prev + 1) % totalPages);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This cleanup function runs whenever isHovered changes
|
||||||
|
// or the component unmounts.
|
||||||
|
return () => {
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
// Move the reset here so it happens "after" the effect cycle
|
||||||
|
if (!isHovered) {
|
||||||
|
setPage(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isHovered, totalPages]);
|
||||||
|
|
||||||
|
const currentMonitors = MONITORS.slice(page * ITEMS_PER_PAGE, (page + 1) * ITEMS_PER_PAGE);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Default View */}
|
||||||
|
<div className="flex min-h-[250px] flex-col justify-center w-full group-hover:opacity-0 group-hover:pointer-events-none transition-all duration-300">
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]" />
|
||||||
|
<span className="font-medium text-white tracking-tight">Hetzner Node-01</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<p className="text-xs text-neutral-500 font-mono">SYS_STATUS:</p>
|
||||||
|
<span className="text-[10px] bg-green-500/10 text-green-500 border border-green-500/20 px-1.5 py-0.5 rounded uppercase font-bold">Online</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Activity className="text-neutral-800 w-10 h-10 -mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Added "Server Specs" to fill space and match the style of "The Architect" */}
|
||||||
|
<div className="mt-8 grid grid-cols-2 gap-4 border-t border-neutral-800/50 pt-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-mono text-neutral-600 uppercase">Architecture</p>
|
||||||
|
<p className="text-xs text-neutral-400">linux/amd64</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-mono text-neutral-600 uppercase">Provider</p>
|
||||||
|
<p className="text-xs text-neutral-400">Hetzner Cloud</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover View */}
|
||||||
|
{/* Hover View: Automated Carousel */}
|
||||||
|
<div className="absolute inset-0 p-5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col bg-neutral-900/95 backdrop-blur-sm">
|
||||||
|
<div className="flex justify-between items-center mb-2 px-1">
|
||||||
|
<h4 className="text-[10px] font-mono text-neutral-500 uppercase tracking-[0.2em]">
|
||||||
|
Service Registry
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Increased height slightly to 200px and removed 'justify-center' from parent */}
|
||||||
|
<div className="flex-1 relative min-h-0 overflow-hidden">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={page}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
className="grid grid-cols-1 gap-2" // Reduced gap from 2 to 1.5
|
||||||
|
>
|
||||||
|
{currentMonitors.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className="flex items-center justify-between bg-neutral-800/30 p-1.5 px-3 rounded-lg border border-neutral-700/30"
|
||||||
|
>
|
||||||
|
<span className="text-[12px] font-medium text-neutral-300 truncate mr-2">
|
||||||
|
{m.name}
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-1 shrink-0 scale-90 origin-right"> {/* Scale badges slightly */}
|
||||||
|
<img src={`https://status.georgew.dev/api/badge/${m.id}/status`} className="h-5" alt="up" />
|
||||||
|
<img src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`} className="h-5 opacity-60" alt="ms" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
data/projects.ts
Normal file
82
data/projects.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { Project } from "@/types/project";
|
||||||
|
|
||||||
|
export const PROJECT_REGISTRY: Project[] = [
|
||||||
|
{
|
||||||
|
slug: "ratoong",
|
||||||
|
category: "web",
|
||||||
|
title: "Ratoong",
|
||||||
|
subtitle: "Regulatory-Compliant Data Platform",
|
||||||
|
role: "Lead Full-Stack Engineer",
|
||||||
|
duration: "2022 — 2024",
|
||||||
|
stack: ["Node.js", "PostgreSQL", "React", "Docker"],
|
||||||
|
metrics: ["99.9% Uptime", "Zero-Data-Loss Integrity", "ISO 27001 Ready"],
|
||||||
|
description: "Architected a high-integrity platform designed to meet rigid regulatory requirements.",
|
||||||
|
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
||||||
|
images: ["/ratoong-hero.jpg", "/ratoong-dashboard.jpg"],
|
||||||
|
liveUrl: "https://ratoong.com",
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "datasaur",
|
||||||
|
category: "web",
|
||||||
|
title: "Datasaur",
|
||||||
|
subtitle: "Personal R&D Pipeline",
|
||||||
|
role: "Architect & Creator",
|
||||||
|
duration: "2025 — Present",
|
||||||
|
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
||||||
|
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
||||||
|
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
||||||
|
images: ["/datasaur-1.jpg"],
|
||||||
|
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||||
|
liveUrl: "https://ratoong.com",
|
||||||
|
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "ayla",
|
||||||
|
category: "web",
|
||||||
|
title: "Ayla",
|
||||||
|
subtitle: "Regulatory-Compliant Data Platform",
|
||||||
|
role: "Lead Full-Stack Engineer",
|
||||||
|
duration: "2022 — 2024",
|
||||||
|
stack: ["Node.js", "PostgreSQL", "React", "Docker"],
|
||||||
|
metrics: ["99.9% Uptime", "Zero-Data-Loss Integrity", "ISO 27001 Ready"],
|
||||||
|
description: "Architected a high-integrity platform designed to meet rigid regulatory requirements.",
|
||||||
|
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
||||||
|
images: ["/ratoong-hero.jpg", "/ratoong-dashboard.jpg"],
|
||||||
|
liveUrl: "https://ratoong.com",
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "flutter-1",
|
||||||
|
category: "mobile",
|
||||||
|
title: "Flutter-1",
|
||||||
|
subtitle: "Personal R&D Pipeline",
|
||||||
|
role: "Architect & Creator",
|
||||||
|
duration: "2025 — Present",
|
||||||
|
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
||||||
|
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
||||||
|
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
||||||
|
images: ["/datasaur-1.jpg"],
|
||||||
|
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||||
|
liveUrl: "https://ratoong.com",
|
||||||
|
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "flutter-2",
|
||||||
|
category: "mobile",
|
||||||
|
title: "Flutter-1",
|
||||||
|
subtitle: "Personal R&D Pipeline",
|
||||||
|
role: "Architect & Creator",
|
||||||
|
duration: "2025 — Present",
|
||||||
|
stack: ["Python", "FastAPI", "Next.js", "Redis"],
|
||||||
|
metrics: ["Sub-50ms Latency", "Automated ETL", "Self-Hosted"],
|
||||||
|
description: "A data science pipeline tool built to explore high-speed processing and real-time visualization of large datasets.",
|
||||||
|
images: ["/datasaur-1.jpg"],
|
||||||
|
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||||
|
liveUrl: "https://ratoong.com",
|
||||||
|
engineeringStory: "In this high-stakes medical environment, I implemented a custom audit logging system that ensured every state change was immutable...",
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
1247
package-lock.json
generated
1247
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
|
"mermaid": "^11.12.2",
|
||||||
"next": "16.1.4",
|
"next": "16.1.4",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
|
|
|
||||||
16
types/project.ts
Normal file
16
types/project.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export interface Project {
|
||||||
|
slug: string;
|
||||||
|
category: 'web' | 'mobile' | 'infrastructure';
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
role: string;
|
||||||
|
duration: string;
|
||||||
|
stack: string[];
|
||||||
|
metrics: string[];
|
||||||
|
description: string;
|
||||||
|
engineeringStory: string;
|
||||||
|
images: string[];
|
||||||
|
liveUrl?: string;
|
||||||
|
repoUrl?: string;
|
||||||
|
isPrivate: boolean;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue