Compare commits
No commits in common. "a09509581f21b858b654fb26d785a1cfb9aab924" and "b0f5d62e3e008548e976b52e5601b1903e4dedd3" have entirely different histories.
a09509581f
...
b0f5d62e3e
8
.vscode/settings.json
vendored
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
|
|
@ -25,9 +24,3 @@ body {
|
|||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.prose {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
|
|||
128
app/page copy 2.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
195
app/page copy.tsx
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
324
app/page.tsx
|
|
@ -1,130 +1,151 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { Globe, Smartphone, Server, Gamepad2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import MonitorCard from "@/components/MonitorCard";
|
||||
import Image from "next/image";
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Globe, Smartphone, Server, Gamepad2, Activity } from 'lucide-react';
|
||||
|
||||
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">
|
||||
|
||||
<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>
|
||||
<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">
|
||||
<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
|
||||
</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
|
||||
</a>
|
||||
{/* <a href="https://github.com/georgew" className="text-neutral-500 hover:text-white transition-colors">
|
||||
GitHub
|
||||
</a> */}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-6">
|
||||
|
||||
{/* 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="absolute top-0 right-0 w-1/2 h-full opacity-[0.03] pointer-events-none"></div>
|
||||
{/* Background Decoration: Subtle Grid or Blueprint */}
|
||||
<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>
|
||||
|
||||
{/* Description and tags */}
|
||||
<div className="flex-[1.5] flex flex-col justify-between relative z-10">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-4 tracking-tight">
|
||||
The Architect
|
||||
</h2>
|
||||
<p className="text-base text-neutral-400 leading-relaxed max-w-lg">
|
||||
Bridging the gap between rigid regulatory requirements and
|
||||
fluid user experiences. I specialize in designing{" "}
|
||||
<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>
|
||||
{/* Left: Bio & Tags */}
|
||||
<div className="flex-[1.5] flex flex-col justify-between relative z-10">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-4 tracking-tight">The Architect</h2>
|
||||
<p className="text-base text-neutral-400 leading-relaxed max-w-lg">
|
||||
Bridging the gap between rigid regulatory requirements and fluid user experiences.
|
||||
I specialize in designing <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="hidden md:block w-px bg-neutral-800/50 self-stretch" />
|
||||
|
||||
{/* Technical details */}
|
||||
<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 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>
|
||||
|
||||
{/* Top Row Right: The Service Registry */}
|
||||
<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>
|
||||
{/* Vertical Divider */}
|
||||
<div className="hidden md:block w-px bg-neutral-800/50 self-stretch" />
|
||||
|
||||
{/* 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 }}
|
||||
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>
|
||||
|
||||
{/* Middle Row: Web Systems */}
|
||||
<Link href="/projects/web" className="group md:col-span-2">
|
||||
<motion.div
|
||||
<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 transition-colors hover:border-blue-500/30"
|
||||
>
|
||||
|
|
@ -132,28 +153,22 @@ export default function Home() {
|
|||
<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 and containerized deployment.
|
||||
Architecting distributed platforms with a focus on high-availability 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 transition-all"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
{['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 transition-all">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
{/* Middle Row: Mobile Apps */}
|
||||
<Link href="/projects/mobile" className="group md:col-span-2">
|
||||
<motion.div
|
||||
<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 transition-colors hover:border-purple-500/30"
|
||||
>
|
||||
|
|
@ -161,28 +176,22 @@ export default function Home() {
|
|||
<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 and native hardware integration.
|
||||
Building fluid, cross-platform experiences using reactive state 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 transition-all"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
{['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 transition-all">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
{/* Middle Row: DevOps */}
|
||||
<Link href="/projects/infrastructure" className="group md:col-span-2">
|
||||
<motion.div
|
||||
<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 transition-colors hover:border-green-500/30"
|
||||
>
|
||||
|
|
@ -190,21 +199,15 @@ export default function Home() {
|
|||
<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 and proactive monitoring.
|
||||
Managing self-hosted cloud nodes with automated CI/CD pipelines 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 transition-all"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
{['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 transition-all">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
|
@ -216,34 +219,33 @@ export default function Home() {
|
|||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<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="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 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>
|
||||
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,177 +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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Mermaid */}
|
||||
{project.mermaidChart && (
|
||||
<section className="mb-16">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="h-px flex-1 bg-neutral-900" />
|
||||
<h3 className="text-[10px] font-mono text-neutral-500 uppercase tracking-[0.3em]">
|
||||
System Architecture Log
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-neutral-900" />
|
||||
</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 */}
|
||||
<section className="w-full pb-20 mt-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">
|
||||
PROJECT LOG // {project.storyLabel || "NARRATIVE"}
|
||||
</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">
|
||||
<ReactMarkdown>{project.engineeringStory}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,119 +1,116 @@
|
|||
"use client";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { Globe, Smartphone, ArrowLeft, Server } from "lucide-react";
|
||||
import { Globe, Smartphone, ArrowLeft } from "lucide-react";
|
||||
import { use } from "react";
|
||||
import { PROJECT_REGISTRY } from "@/data/projects";
|
||||
|
||||
const CATEGORY_META = {
|
||||
const categories = {
|
||||
web: {
|
||||
title: "Web Systems",
|
||||
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: {
|
||||
title: "Mobile Apps",
|
||||
icon: <Smartphone className="w-8 h-8 text-purple-400" />,
|
||||
description:
|
||||
"Building cross-platform experiences with Flutter and native integrations.",
|
||||
},
|
||||
infrastructure: {
|
||||
title: "DevOps & Infrastructure",
|
||||
icon: <Server className="w-8 h-8 text-green-400" />,
|
||||
description:
|
||||
"Self-hosted systems architecture and automated deployment pipelines.",
|
||||
},
|
||||
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 }>;
|
||||
}) {
|
||||
export default function CategoryPage({ params }: { params: Promise<{ category: string }> }) {
|
||||
|
||||
|
||||
const resolvedParams = use(params);
|
||||
const category = resolvedParams.category;
|
||||
|
||||
const meta = CATEGORY_META[category as keyof typeof CATEGORY_META];
|
||||
const data = categories[category as keyof typeof categories];
|
||||
|
||||
const filteredProjects = PROJECT_REGISTRY.filter(
|
||||
(p) => p.category === category,
|
||||
);
|
||||
if (!data) return <div className="p-24 text-white">Category not found.</div>;
|
||||
|
||||
if (!meta) {
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="p-24 text-white font-mono">
|
||||
<h1 className="text-2xl mb-4">404: Category Not Found</h1>
|
||||
<Link href="/" className="text-blue-400 underline mt-4 block">
|
||||
Return Home
|
||||
</Link>
|
||||
<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"
|
||||
>
|
||||
<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
|
||||
<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">
|
||||
{meta.icon}
|
||||
<h1 className="text-5xl font-bold">{meta.title}</h1>
|
||||
{data.icon}
|
||||
<h1 className="text-5xl font-bold">{data.title}</h1>
|
||||
</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">{data.description}</p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8">
|
||||
{filteredProjects.map((project, index) => (
|
||||
<Link
|
||||
key={project.slug}
|
||||
href={`/projects/${category}/${project.slug}`}
|
||||
{data.projects.map((project, index) => (
|
||||
<motion.div
|
||||
key={project.name}
|
||||
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"
|
||||
>
|
||||
<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.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 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>
|
||||
</motion.div>
|
||||
</Link>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
interface GalleryProps {
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export default function ImageCarousel({ images }: GalleryProps) {
|
||||
const [[page, direction], setPage] = useState([0, 0]);
|
||||
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
||||
|
||||
const imageIndex = Math.abs(page % images.length);
|
||||
|
||||
const paginate = useCallback(
|
||||
(newDirection: number) => {
|
||||
setPage([page + newDirection, newDirection]);
|
||||
},
|
||||
[page],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAutoPlaying) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
paginate(1);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [paginate, isAutoPlaying]);
|
||||
|
||||
const variants = {
|
||||
enter: (direction: number) => ({
|
||||
x: direction > 0 ? 1000 : -1000,
|
||||
opacity: 0,
|
||||
}),
|
||||
center: { zIndex: 1, x: 0, opacity: 1 },
|
||||
exit: (direction: number) => ({
|
||||
zIndex: 0,
|
||||
x: direction < 0 ? 1000 : -1000,
|
||||
opacity: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative aspect-video w-full overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900"
|
||||
onMouseEnter={() => setIsAutoPlaying(false)}
|
||||
onMouseLeave={() => setIsAutoPlaying(true)}
|
||||
>
|
||||
<AnimatePresence initial={false} custom={direction}>
|
||||
<motion.img
|
||||
key={page}
|
||||
src={images[imageIndex]}
|
||||
custom={direction}
|
||||
variants={variants}
|
||||
initial="enter"
|
||||
animate="center"
|
||||
exit="exit"
|
||||
transition={{
|
||||
x: { type: "spring", stiffness: 300, damping: 30 },
|
||||
opacity: { duration: 0.2 },
|
||||
}}
|
||||
drag="x"
|
||||
dragConstraints={{ left: 0, right: 0 }}
|
||||
dragElastic={1}
|
||||
onDragStart={() => setIsAutoPlaying(false)}
|
||||
onDragEnd={(e, { offset }) => {
|
||||
const swipe = Math.abs(offset.x) > 50;
|
||||
if (swipe) paginate(offset.x > 0 ? -1 : 1);
|
||||
}}
|
||||
className="absolute h-full w-full object-cover cursor-grab active:cursor-grabbing"
|
||||
/>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Navigation Arrows */}
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-between p-4 pointer-events-none">
|
||||
<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"
|
||||
onClick={() => {
|
||||
setIsAutoPlaying(false);
|
||||
paginate(-1);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
</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"
|
||||
onClick={() => {
|
||||
setIsAutoPlaying(false);
|
||||
paginate(1);
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{isAutoPlaying && (
|
||||
<motion.div
|
||||
key={imageIndex}
|
||||
initial={{ width: "0%" }}
|
||||
animate={{ width: "100%" }}
|
||||
transition={{ duration: 5, ease: "linear" }}
|
||||
className="absolute bottom-0 left-0 h-1 bg-blue-500/50 z-20"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import mermaid from "mermaid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Maximize2, Minimize2 } from "lucide-react";
|
||||
|
||||
export default function Mermaid({ chart }: { chart: string }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: "dark",
|
||||
securityLevel: "loose",
|
||||
fontFamily: "monospace",
|
||||
});
|
||||
|
||||
// Render the chart once. Use a timeout to ensure DOM is stable.
|
||||
const renderChart = async () => {
|
||||
try {
|
||||
await mermaid.contentLoaded();
|
||||
setIsRendered(true);
|
||||
} catch (err) {
|
||||
console.error("Mermaid render failed:", err);
|
||||
}
|
||||
};
|
||||
|
||||
renderChart();
|
||||
}, [chart]); // Only re-run if the chart string itself changes
|
||||
|
||||
return (
|
||||
<div className="relative max-w-4xl mx-auto group">
|
||||
<motion.div
|
||||
layout
|
||||
initial={false}
|
||||
animate={{
|
||||
height: isExpanded ? "auto" : "400px",
|
||||
}}
|
||||
transition={{ duration: 0.6, ease: [0.23, 1, 0.32, 1] }}
|
||||
className={`relative bg-neutral-900/20 rounded-3xl border border-neutral-800/50 overflow-hidden transition-colors duration-500 ${
|
||||
!isExpanded
|
||||
? "hover:border-neutral-700 cursor-pointer"
|
||||
: "cursor-default"
|
||||
}`}
|
||||
onClick={() => !isExpanded && setIsExpanded(true)}
|
||||
>
|
||||
{/* 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 pointer-events-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{/* Chart Area */}
|
||||
<div
|
||||
className={`p-4 md:p-12 transition-opacity duration-500 ${isRendered ? "opacity-100" : "opacity-0"}`}
|
||||
>
|
||||
<div className="mermaid flex justify-center">{chart}</div>
|
||||
</div>
|
||||
|
||||
{/* Fade Overlay */}
|
||||
<AnimatePresence>
|
||||
{!isExpanded && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/90 to-transparent pointer-events-none"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* Toggle Button */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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
|
||||
${
|
||||
isExpanded
|
||||
? "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 ? (
|
||||
<>
|
||||
<Minimize2 size={12} /> Collapse Logic
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Maximize2 size={12} /> Expand Architecture
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
"use client";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import mermaid from "mermaid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Maximize2, Minimize2 } from "lucide-react";
|
||||
|
||||
export default function Mermaid({ chart }: { chart: string }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [needsExpansion, setNeedsExpansion] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: "dark",
|
||||
securityLevel: "loose",
|
||||
fontFamily: "monospace",
|
||||
});
|
||||
mermaid.contentLoaded();
|
||||
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.scrollHeight;
|
||||
setNeedsExpansion(height > 400);
|
||||
}
|
||||
}, [chart]);
|
||||
|
||||
return (
|
||||
<div className="relative max-w-4xl mx-auto group">
|
||||
<motion.div
|
||||
initial={false}
|
||||
onClick={() => needsExpansion && setIsExpanded(!isExpanded)}
|
||||
animate={{ height: isExpanded || !needsExpansion ? "auto" : "400px" }}
|
||||
className={`relative bg-neutral-900/20 rounded-3xl border border-neutral-800/50 p-4 md:p-12 overflow-hidden transition-colors duration-500
|
||||
${needsExpansion && !isExpanded ? "cursor-pointer hover:border-neutral-700" : "cursor-default"}`}
|
||||
>
|
||||
{/* Legend */}
|
||||
<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="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<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 ref={contentRef} className="mermaid flex justify-center">
|
||||
{chart}
|
||||
</div>
|
||||
|
||||
{/* The "Fade to Darkness" Overlay (when expansion is needed) */}
|
||||
<AnimatePresence>
|
||||
{needsExpansion && !isExpanded && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/80 to-transparent pointer-events-none"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* Expand/Collapse Button (when expansion is needed) */}
|
||||
{needsExpansion && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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
|
||||
${
|
||||
isExpanded
|
||||
? "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 ? (
|
||||
<>
|
||||
{" "}
|
||||
<Minimize2 size={12} /> Collapse Logic{" "}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<Maximize2 size={12} /> Expand Architecture{" "}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
"use client";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import mermaid from "mermaid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Maximize2, Minimize2 } from "lucide-react";
|
||||
|
||||
export default function Mermaid({ chart }: { chart: string }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [needsExpansion, setNeedsExpansion] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: "dark",
|
||||
securityLevel: "loose",
|
||||
fontFamily: "monospace",
|
||||
});
|
||||
mermaid.contentLoaded();
|
||||
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.scrollHeight;
|
||||
setNeedsExpansion(height > 400);
|
||||
}
|
||||
}, [chart]);
|
||||
|
||||
return (
|
||||
<div className="relative max-w-4xl mx-auto group">
|
||||
<motion.div
|
||||
initial={false}
|
||||
onClick={() => needsExpansion && setIsExpanded(!isExpanded)}
|
||||
animate={{ height: isExpanded || !needsExpansion ? "auto" : "400px" }}
|
||||
className={`relative bg-neutral-900/20 rounded-3xl border border-neutral-800/50 p-4 md:p-12 overflow-hidden transition-colors duration-500
|
||||
${needsExpansion && !isExpanded ? "cursor-pointer hover:border-neutral-700" : "cursor-default"}`}
|
||||
>
|
||||
{/* Legend */}
|
||||
<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="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<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 ref={contentRef} className="mermaid flex justify-center">
|
||||
{chart}
|
||||
</div>
|
||||
|
||||
{/* The "Fade to Darkness" Overlay (when expansion is needed) */}
|
||||
<AnimatePresence>
|
||||
{needsExpansion && !isExpanded && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/80 to-transparent pointer-events-none"
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* Expand/Collapse Button (when expansion is needed) */}
|
||||
{needsExpansion && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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
|
||||
${
|
||||
isExpanded
|
||||
? "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 ? (
|
||||
<>
|
||||
{" "}
|
||||
<Minimize2 size={12} /> Collapse Logic{" "}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<Maximize2 size={12} /> Expand Architecture{" "}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Activity } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
const MONITORS = [
|
||||
{ id: 2, name: "Datasaur" },
|
||||
{ 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) {
|
||||
interval = setInterval(() => {
|
||||
setPage((prev) => (prev + 1) % totalPages);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
if (!isHovered) {
|
||||
setPage(0);
|
||||
}
|
||||
};
|
||||
}, [isHovered, totalPages]);
|
||||
|
||||
const currentMonitors = MONITORS.slice(
|
||||
page * ITEMS_PER_PAGE,
|
||||
(page + 1) * ITEMS_PER_PAGE,
|
||||
);
|
||||
|
||||
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>
|
||||
|
||||
<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 */}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function ProjectShowcase({ images }: { images: string[] }) {
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 h-[500px]">
|
||||
{/* Main Image */}
|
||||
<div className="lg:col-span-9 relative overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900 group">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.img
|
||||
key={index}
|
||||
src={images[index]}
|
||||
initial={{ opacity: 0, scale: 1.05 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 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">
|
||||
<p className="text-[10px] font-mono uppercase tracking-widest text-white/70">
|
||||
View {index + 1} of {images.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{images.map((img, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setIndex(i)}
|
||||
className={`relative flex-shrink-0 w-24 lg:w-full aspect-video rounded-xl border-2 transition-all overflow-hidden ${
|
||||
i === index
|
||||
? "border-blue-500 ring-4 ring-blue-500/10"
|
||||
: "border-neutral-800 opacity-40 hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={img}
|
||||
className="h-full w-full object-cover"
|
||||
alt={`Thumb ${i}`}
|
||||
/>
|
||||
{i === index && (
|
||||
<motion.div
|
||||
layoutId="active-thumb"
|
||||
className="absolute inset-0 bg-blue-500/10 z-10"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
285
data/projects.ts
|
|
@ -1,285 +0,0 @@
|
|||
import { Project } from "@/types/project";
|
||||
|
||||
export const PROJECT_REGISTRY: Project[] = [
|
||||
{
|
||||
slug: "ratoong",
|
||||
category: "web",
|
||||
title: "Ratoong",
|
||||
subtitle: "High-Performance Ski & Travel Engine",
|
||||
role: "Full-Stack Engineer",
|
||||
duration: "2020 — 2022",
|
||||
stack: ["Angular", "Firebase", "GCP Cloud Functions", "TypeScript"],
|
||||
metrics: [
|
||||
"< 200ms Search Latency",
|
||||
"10,000+ Active Data Points",
|
||||
"Fully Responsive Design",
|
||||
],
|
||||
description:
|
||||
"A comprehensive ski resort planning and rating platform featuring real-time weather integration and complex multi-parameter search filters.",
|
||||
engineeringStory: `
|
||||
Building Ratoong was an exercise in managing **High-Density Data** within a reactive frontend ecosystem. The core challenge was transforming thousands of resort data points—ranging from piste lengths to real-time weather—into a lightning-fast, searchable interface.
|
||||
|
||||
#### Data Orchestration & Efficiency
|
||||
Leveraging a **Document-Based Architecture (Firestore)**, I designed a schema that balanced read efficiency with real-time updates. To handle complex filtering (altitude, lift types, pricing) without taxing the client-side, I utilized **GCP Cloud Functions** as a middleware layer to process and normalize data from various 3rd-party APIs, including Google Maps and Weather services.
|
||||
|
||||
#### Modern Angular & Responsive UI
|
||||
The frontend was built using modern **Angular**, focusing on a component-based architecture that ensured high performance across both desktop and mobile. I implemented a custom state management flow to handle resort ratings and trip planning, ensuring that user interactions were instantly reflected in the UI while syncing seamlessly with **Firebase Authentication**.
|
||||
|
||||
#### Lessons in Scalability
|
||||
Working with a **Backend-as-a-Service (BaaS)** model taught me the importance of cost-effective query design and the power of event-driven triggers. I was responsible for maintaining the development, staging, and production environments, ensuring a clean CI/CD flow from localhost to the Firebase cloud.
|
||||
|
||||
#### Security & Data Governance
|
||||
A key architectural pillar was the implementation of a robust **Security Rules** layer within Firebase. By moving the logic from the client to the database level, we ensured that resort metadata was globally searchable while sensitive user planning data remained strictly isolated. This event-driven security model allowed us to scale the user base without increasing the risk surface area of the platform.
|
||||
`,
|
||||
images: [
|
||||
"/projects/ratoong/ratoong-1.jpg",
|
||||
"/projects/ratoong/ratoong-2.jpg",
|
||||
"/projects/ratoong/ratoong-3.jpg",
|
||||
"/projects/ratoong/ratoong-4.jpg",
|
||||
"/projects/ratoong/ratoong-5.jpg",
|
||||
],
|
||||
liveUrl: "https://ratoong.com",
|
||||
isPrivate: false,
|
||||
mermaidChart: `
|
||||
graph LR
|
||||
subgraph Client_Side [Frontend]
|
||||
A[Angular Web App]:::traffic
|
||||
end
|
||||
|
||||
subgraph Firebase_GCP [Cloud Infrastructure]
|
||||
direction TB
|
||||
Hub((Firebase SDK)):::hub
|
||||
|
||||
B[Firebase Auth]:::node
|
||||
C[Firestore DB]:::node
|
||||
D[Cloud Functions]:::node
|
||||
E[Partner API Proxy]:::node
|
||||
end
|
||||
|
||||
subgraph External [Third Party]
|
||||
direction TB
|
||||
F[Weather API]:::traffic
|
||||
G[Google Maps API]:::traffic
|
||||
H[Affiliate Partners]:::traffic
|
||||
end
|
||||
|
||||
A ==> Hub
|
||||
Hub -->|Identity| B
|
||||
Hub <-->|Data Sync| C
|
||||
Hub -->|Triggers| D
|
||||
|
||||
D --> F
|
||||
D --> G
|
||||
D --> H
|
||||
E -.->|Internal Access| C
|
||||
|
||||
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
|
||||
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
|
||||
classDef hub fill:#f59e0b,stroke:#d97706,color:#fff,stroke-width:2px
|
||||
`,
|
||||
storyLabel: "DATA // UI EFFICIENCY",
|
||||
},
|
||||
{
|
||||
slug: "datasaur",
|
||||
category: "web",
|
||||
title: "Datasaur",
|
||||
subtitle: "Automated Statistical Analysis Engine",
|
||||
role: "Lead Architect & Creator",
|
||||
duration: "2019 — 2021", // Reflecting "one of my first things"
|
||||
stack: ["Python", "Flask", "MongoDB", "Pandas", "SciPy"],
|
||||
metrics: [
|
||||
"Automated Stat-Testing",
|
||||
"Multi-Format ETL",
|
||||
"Self-Hosted Architecture",
|
||||
],
|
||||
description:
|
||||
"A comprehensive survey data platform that automates complex statistical workflows, from raw data aggregation to advanced hypothesis testing and visualization.",
|
||||
storyLabel: "ALGORITHMIC // STATISTICAL PROCESSING",
|
||||
images: [
|
||||
"/projects/datasaur/datasaur-1.jpg",
|
||||
"/projects/datasaur/datasaur-2.jpg",
|
||||
"/projects/datasaur/datasaur-3.jpg",
|
||||
"/projects/datasaur/datasaur-4.jpg",
|
||||
"/projects/datasaur/datasaur-5.jpg",
|
||||
"/projects/datasaur/datasaur-6.jpg",
|
||||
],
|
||||
repoUrl: "https://git.georgew.dev/georgew/datasaur",
|
||||
liveUrl: "https://datasaur.georgew.dev", // Adjusted based on your self-hosting mention
|
||||
isPrivate: false,
|
||||
engineeringStory: `
|
||||
Datasaur was born out of a necessity to bridge the gap between raw survey data and academic-grade statistical insights. The challenge wasn't just displaying data, but architecting a system capable of performing complex mathematical computations on-the-fly.
|
||||
|
||||
#### Statistical Automation Pipeline
|
||||
The core of the application is a robust processing engine built on **Pandas** and **SciPy**. I implemented automated workflows for non-parametric tests like **Kruskal-Wallis** and **Mann-Whitney U**, ensuring that the platform could intelligently suggest and execute the correct statistical test based on the data distribution.
|
||||
|
||||
#### Data Visualization & Export
|
||||
To translate these numbers into insights, I built a visualization layer supporting everything from standard histograms to complex **Box and Whisker** plots. Using **XlsxWriter**, I developed a custom export engine that allowed users to pull processed data directly into professional-grade spreadsheets with pre-formatted statistical summaries.
|
||||
|
||||
#### Infrastructure & Monolithic Integrity
|
||||
The project follows a classic monolithic architecture, which proved highly efficient for keeping memory-intensive dataframes close to the processing logic. Today, the platform is self-hosted using a **Caddy** reverse proxy and **MongoDB Atlas**, demonstrating the longevity and stability of a well-architected Flask ecosystem.
|
||||
`,
|
||||
mermaidChart: `
|
||||
graph LR
|
||||
subgraph Client_Layer [User Interface]
|
||||
A[Vanilla JS / Browser]:::traffic
|
||||
end
|
||||
|
||||
subgraph Server_Layer [Application Logic]
|
||||
B[Caddy Reverse Proxy]:::node
|
||||
C[Flask / Python Monolith]:::node
|
||||
end
|
||||
|
||||
subgraph Processing_Engine [Data Science Core]
|
||||
D[Pandas ETL]:::node
|
||||
E[SciPy / Pingouin Stats]:::node
|
||||
F[XlsxWriter Export]:::node
|
||||
end
|
||||
|
||||
subgraph Storage [Data Persistence]
|
||||
G[MongoDB Atlas]:::node
|
||||
end
|
||||
|
||||
A <-->|HTTPS| B
|
||||
B <-->|WSGI| C
|
||||
C <-->|Query/Write| G
|
||||
C ==>|Dataframes| D
|
||||
D --> E
|
||||
D --> F
|
||||
|
||||
%% Styles %%
|
||||
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
|
||||
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
|
||||
`,
|
||||
},
|
||||
{
|
||||
slug: "ayla",
|
||||
category: "infrastructure",
|
||||
title: "Ayla",
|
||||
subtitle: "Regulatory-Compliant Medical Platform",
|
||||
role: "Tech Lead & Scrum Master",
|
||||
duration: "2022 — 2024",
|
||||
stack: [
|
||||
"Kubernetes",
|
||||
"Ruby on Rails",
|
||||
"Flutter",
|
||||
"Terraform",
|
||||
"GCP",
|
||||
"OTC",
|
||||
],
|
||||
metrics: [
|
||||
"Multi-Region Data Residency",
|
||||
"ISO 27001 Compliant",
|
||||
"Single-Click IaC Deployment",
|
||||
],
|
||||
description:
|
||||
"A high-availability medical device platform supporting dementia treatment, featuring multi-cloud infrastructure, automated pipelines, cross-platform web and mobile deployments and strict regulatory requirements.",
|
||||
storyLabel: "GOVERNANCE // CLOUD ORCHESTRATION",
|
||||
images: [
|
||||
"/projects/ayla/ayla-1.jpg",
|
||||
"/projects/ayla/ayla-2.jpg",
|
||||
"/projects/ayla/ayla-3.jpg",
|
||||
"/projects/ayla/ayla-4.jpg",
|
||||
"/projects/ayla/ayla-5.jpg",
|
||||
],
|
||||
isPrivate: true,
|
||||
engineeringStory: `
|
||||
As Tech Lead for Ayla, I was responsible for architecting a platform that met the rigorous safety and security standards of a certified medical device. This required a "Security-by-Design" approach, balancing high availability (SLA) with rigid data residency requirements across the UK and EU.
|
||||
|
||||
#### Multi-Cloud Infrastructure & IaC
|
||||
To satisfy GDPR and local health data regulations, I architected a dual-cloud strategy: **Open Telekom Cloud (OTC)** for European users and **GCP** for the UK. Using **Terraform**, I codified the entire infrastructure, enabling us to spin up identical, audit-ready Kubernetes clusters or Cloud Run environments in minutes. This automation was critical for maintaining the "Release-Pre-Release" protocols required for medical certification.
|
||||
|
||||
#### Full-Stack Delivery & Automation
|
||||
The platform featured a **Flutter** frontend for Web, iOS, and Android, all managed through automated **CICD** pipelines. I implemented a layered automation strategy, combining **GitHub Actions** for web deployments and server-side logic with **Fastlane** for mobile app store distribution. The backend was a high-performance **Ruby on Rails** API, architected as a stateless "mini-service" to ensure horizontal scalability within Kubernetes. I also integrated **Squidex CMS** to empower non-technical colleagues to manage content without compromising the system's core integrity.
|
||||
|
||||
#### Leadership & Compliance
|
||||
Beyond the code, I served as Scrum Master and Product Owner, leading sprint planning, retro and demos. I worked closely with regulatory partners and personally oversaw the creation of **DPIAs**, **Cyber Essentials** certification, and the path to **ISO 27001** compliance. In the absence of a dedicated IT department, I managed the MDM systems and sysadmin duties, ensuring that every layer of the organization met the strict regulatory bar.
|
||||
`,
|
||||
mermaidChart: `
|
||||
graph TB
|
||||
%% Direction and Layout
|
||||
direction TB
|
||||
|
||||
subgraph Shared_Ops [DevOps & CMS]
|
||||
I[GitHub Actions CICD]:::traffic
|
||||
J[Terraform IaC]:::traffic
|
||||
K[Squidex CMS]:::node
|
||||
end
|
||||
|
||||
subgraph Frontend_Layer [Omni-Channel]
|
||||
A[Flutter Web / Mobile]:::traffic
|
||||
B[Bunny CDN / Edge Storage]:::node
|
||||
end
|
||||
|
||||
subgraph UK_Region [GCP]
|
||||
G[Cloud Run Containers]:::node
|
||||
H[Cloud SQL]:::node
|
||||
end
|
||||
|
||||
subgraph EU_Region [Open Telekom Cloud]
|
||||
D[NGINX Ingress]:::node
|
||||
C[K8s Cluster]:::node
|
||||
F[Object Storage]:::node
|
||||
E[PostgreSQL RDS]:::node
|
||||
end
|
||||
|
||||
%% Connections
|
||||
A <--> B
|
||||
I -->|Fastlane| A
|
||||
J -->|Provision| G
|
||||
J -->|Provision| C
|
||||
|
||||
B <-->|UK Traffic| G
|
||||
B <-->|EU Traffic| D
|
||||
|
||||
D --> C
|
||||
G <--> H
|
||||
C <--> E
|
||||
C --- F
|
||||
|
||||
G <--> K
|
||||
C <--> K
|
||||
|
||||
%% Styles
|
||||
classDef traffic fill:#2563eb,stroke:#3b82f6,color:#fff
|
||||
classDef node fill:#16a34a,stroke:#22c55e,color:#fff
|
||||
`,
|
||||
},
|
||||
{
|
||||
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...",
|
||||
storyLabel: "DATA EFFICIENCY",
|
||||
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...",
|
||||
storyLabel: "DATA EFFICIENCY",
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
|
@ -1,13 +1,18 @@
|
|||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
eslintConfigPrettier,
|
||||
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
|
|
|
|||
3172
package-lock.json
generated
10
package.json
|
|
@ -9,26 +9,20 @@
|
|||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/upgrade": "^4.1.18",
|
||||
"framer-motion": "^12.29.2",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "16.1.4",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"react-markdown": "^10.1.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 350 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 292 KiB |
|
|
@ -1,18 +0,0 @@
|
|||
export interface Project {
|
||||
slug: string;
|
||||
category: 'web' | 'mobile' | 'infrastructure';
|
||||
title: string;
|
||||
subtitle: string;
|
||||
role: string;
|
||||
duration: string;
|
||||
stack: string[];
|
||||
metrics: string[];
|
||||
description: string;
|
||||
engineeringStory: string;
|
||||
storyLabel?: string;
|
||||
images: string[];
|
||||
liveUrl?: string;
|
||||
repoUrl?: string;
|
||||
mermaidChart?: string;
|
||||
isPrivate: boolean;
|
||||
}
|
||||