diff --git a/app/lab/page.tsx b/app/lab/page.tsx new file mode 100644 index 0000000..531f6e3 --- /dev/null +++ b/app/lab/page.tsx @@ -0,0 +1,136 @@ +"use client"; +import { LAB_SERVICES } from "@/data/lab"; +import { ExternalLink, Lock, Box, Terminal, Globe } from "lucide-react"; +import { motion } from "framer-motion"; +import Image from "next/image"; +import PageLayout from "@/components/PageLayout"; + +export default function LabPage() { + return ( + +
+
+ +

+ System_Lab +

+
+

+ A registry of operational services and experimental R&D. Services + labeled + [VPN] are secured via + Tailscale to maintain a hardened perimeter for sensitive telemetry. +

+
+ +
+ {" "} + {/* Increased spacing for alternating rhythm */} + {LAB_SERVICES.map((service, i) => { + const isEven = i % 2 === 0; + return ( + + {/* Image Side */} +
+
+ {service.name} +
+
+ + {/* Text Side */} +
+
+
+

+ {service.name} +

+ + {/* Live Status Badge */} + {service.uptimeId && ( +
+ Online +
+ )} +
+ +
+ {service.stack.map((tech) => ( + + {tech} + + ))} +
+
+ +

+ {service.description} +

+ + {/* Conditional Actions */} +
+ {service.visibility === "public" && service.url ? ( + + Visit Service + + ) : ( +
+ VPN + Encrypted +
+ )} + + {service.gitUrl && ( + + Forgejo + {`Source`} + + )} +
+
+
+ ); + })} +
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index cb7780f..d326cff 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -126,15 +126,17 @@ export default function Home() { {/* Top Row Right: The Service Registry */} - 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 min-h-[180px]" - > - - + + setIsHoveringMonitors(true)} + onMouseLeave={() => setIsHoveringMonitors(false)} + className="flex-1 p-6 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-center relative overflow-hidden min-h-[180px] hover:border-blue-500/30" + > + + + {/* Project Category Cards */} { + let interval: NodeJS.Timeout; + + if (isHovered) { + // Start rotating pages only when hovered + interval = setInterval(() => { + setPage((p) => (p + 1) % totalPages); + }, INTERVAL_TIME); + } else { + // Defer state reset to avoid "cascading render" error + // and allow the fade-out animation to play smoothly + const timeout = setTimeout(() => { + setPage(0); + }, 300); + return () => clearTimeout(timeout); + } + + return () => { + if (interval) clearInterval(interval); + }; + }, [isHovered, totalPages]); + return (
- {/* DEFAULT VIEW: Always rendered, opacity controlled by parent hover */} + {/* --- DEFAULT VIEW --- */}
@@ -58,7 +88,7 @@ export default function MonitorCard({ isHovered }: { isHovered: boolean }) {
- {/* REGISTRY VIEW: Only "Active" when hovered */} + {/* --- REGISTRY VIEW --- */}
-

- Service Registry -

+ {/* HEADER WITH SYNCED TIMER */} +
+

+ Explore Systems + + → + +

+ +
+
+ +
+ + {String(page + 1).padStart(2, "0")} + +
+
+
- {/* We pass isHovered here to start/stop the timer inside a separate lifecycle */} - +
); } -function RegistrySlider({ isHovered }: { isHovered: boolean }) { - const [page, setPage] = useState(0); - const totalPages = Math.ceil(MONITORS.length / 6); - - useEffect(() => { - if (!isHovered) { - const timeout = setTimeout(() => setPage(0), 300); - return () => clearTimeout(timeout); - } - - const interval = setInterval(() => { - setPage((p) => (p + 1) % totalPages); - }, 4000); - - return () => clearInterval(interval); - }, [isHovered, totalPages]); - - const currentItems = MONITORS.slice(page * 6, (page + 1) * 6); +function RegistrySlider({ page }: { page: number }) { + const currentItems = MONITORS.slice( + page * ITEMS_PER_PAGE, + (page + 1) * ITEMS_PER_PAGE, + ); return (
System Status \ No newline at end of file diff --git a/public/lab/audiobookshelf.jpg b/public/lab/audiobookshelf.jpg new file mode 100644 index 0000000..21b56dd Binary files /dev/null and b/public/lab/audiobookshelf.jpg differ diff --git a/public/lab/change-detection.jpg b/public/lab/change-detection.jpg new file mode 100644 index 0000000..1c0bc5f Binary files /dev/null and b/public/lab/change-detection.jpg differ diff --git a/public/lab/observatory.jpg b/public/lab/observatory.jpg new file mode 100644 index 0000000..ffb6bb0 Binary files /dev/null and b/public/lab/observatory.jpg differ diff --git a/public/lab/paperless.jpg b/public/lab/paperless.jpg new file mode 100644 index 0000000..9b531f9 Binary files /dev/null and b/public/lab/paperless.jpg differ diff --git a/public/lab/portainer.jpg b/public/lab/portainer.jpg new file mode 100644 index 0000000..a3c52c6 Binary files /dev/null and b/public/lab/portainer.jpg differ diff --git a/public/lab/surf-hub.jpg b/public/lab/surf-hub.jpg new file mode 100644 index 0000000..6e7a6ae Binary files /dev/null and b/public/lab/surf-hub.jpg differ diff --git a/public/lab/wikijs.jpg b/public/lab/wikijs.jpg new file mode 100644 index 0000000..9d267e3 Binary files /dev/null and b/public/lab/wikijs.jpg differ diff --git a/public/lab/yamtrack.jpg b/public/lab/yamtrack.jpg new file mode 100644 index 0000000..2775832 Binary files /dev/null and b/public/lab/yamtrack.jpg differ diff --git a/types/index.ts b/types/index.ts index d557119..effeca5 100644 --- a/types/index.ts +++ b/types/index.ts @@ -35,3 +35,15 @@ export interface Project { mermaidChart?: string; isPrivate: boolean; } + +export interface LabService { + id: string; + name: string; + description: string; + stack: string[]; + visibility: "public" | "tailscale"; + url?: string; + gitUrl?: string; + image: string; + uptimeId?: number; +}