portfolio/components/MonitorCard.tsx
2026-01-29 20:54:37 +01:00

117 lines
4.5 KiB
TypeScript

'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>
</>
);
}