Added project details pages. Cleaned up dashboard
This commit is contained in:
parent
b0f5d62e3e
commit
5d0a86645d
70
app/page.tsx
70
app/page.tsx
|
|
@ -3,8 +3,12 @@
|
|||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Globe, Smartphone, Server, Gamepad2, Activity } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import MonitorCard from '@/components/MonitorCard';
|
||||
|
||||
export default function Home() {
|
||||
const [isHoveringMonitors, setIsHoveringMonitors] = useState(false);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
|
||||
<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">
|
||||
LinkedIn
|
||||
</a>
|
||||
{/* <a href="https://github.com/georgew" className="text-neutral-500 hover:text-white transition-colors">
|
||||
GitHub
|
||||
</a> */}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -87,61 +88,14 @@ export default function Home() {
|
|||
|
||||
|
||||
{/* Top Row Right: The Service Registry (Restored) */}
|
||||
<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 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
|
||||
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]"
|
||||
>
|
||||
<MonitorCard isHovered={isHoveringMonitors} />
|
||||
</motion.div>
|
||||
|
||||
{/* Middle Row: Web Systems */}
|
||||
<Link href="/projects/web" className="group md:col-span-2">
|
||||
|
|
|
|||
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";
|
||||
import { motion } from "framer-motion";
|
||||
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 { PROJECT_REGISTRY } from "@/data/projects";
|
||||
|
||||
const categories = {
|
||||
const CATEGORY_META = {
|
||||
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"]
|
||||
}
|
||||
]
|
||||
},
|
||||
infrastructure: {
|
||||
title: "DevOps & Infrastructure",
|
||||
icon: <Server className="w-8 h-8 text-green-400" />,
|
||||
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 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 (!data) {
|
||||
if (!meta) {
|
||||
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">
|
||||
|
|
@ -74,15 +57,18 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
|
|||
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>
|
||||
{meta.icon}
|
||||
<h1 className="text-5xl font-bold">{meta.title}</h1>
|
||||
</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">
|
||||
{data.projects.map((project, index) => (
|
||||
{filteredProjects.map((project, index) => (
|
||||
<Link
|
||||
key={project.slug}
|
||||
href={`/projects/${category}/${project.slug}`}>
|
||||
<motion.div
|
||||
key={project.name}
|
||||
|
||||
layout
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
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>
|
||||
<h3 className="text-2xl font-semibold mb-2">{project.name}</h3>
|
||||
<p className="text-neutral-500">{project.detail}</p>
|
||||
<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 => (
|
||||
|
|
@ -108,6 +94,7 @@ export default function CategoryPage({ params }: { params: Promise<{ category: s
|
|||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
))}
|
||||
</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": {
|
||||
"framer-motion": "^12.29.2",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "16.1.4",
|
||||
"react": "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