portfolio/app/lab/page.tsx
2026-02-02 18:55:07 +01:00

137 lines
5.6 KiB
TypeScript

"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 (
<PageLayout backLink="/" maxWidth="6xl">
<header className="mb-32">
<div className="flex items-center gap-2 mb-4">
<Box className="text-blue-500" size={20} />
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase">
System_Lab
</h1>
</div>
<p className="text-neutral-500 max-w-2xl leading-relaxed text-sm">
A registry of operational services and experimental R&D. Services
labeled
<span className="text-blue-500"> [VPN]</span> are secured via
Tailscale to maintain a hardened perimeter for sensitive telemetry.
</p>
</header>
<div className="space-y-40 mb-20">
{" "}
{/* Increased spacing for alternating rhythm */}
{LAB_SERVICES.map((service, i) => {
const isEven = i % 2 === 0;
return (
<motion.section
key={service.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
className={`flex flex-col ${isEven ? "md:flex-row" : "md:flex-row-reverse"} gap-12 md:gap-24 items-center group`}
>
{/* Image Side */}
<div className="w-full md:w-1/2">
<div className="relative group aspect-[1.9/1] rounded-2xl overflow-hidden border border-neutral-800 bg-black shadow-2xl transition-colors hover:border-blue-500/50">
<Image
src={service.image}
alt={service.name}
fill
className="object-cover transition-all duration-500 ease-out brightness-100 grayscale-[0.2] group-hover:grayscale-0 scale-100 group-hover:scale-105"
/>
</div>
</div>
{/* Text Side */}
<div className="w-full md:w-1/2 space-y-6">
<div className="space-y-2">
<div className="flex items-center flex-wrap gap-3">
<h3 className="text-2xl font-bold text-white tracking-tight">
{service.name}
</h3>
{/* Live Status Badge */}
{service.uptimeId && (
<div className="flex items-center shrink-0">
<Image
src={`https://status.georgew.dev/api/badge/${service.uptimeId}/status`}
alt="Online"
width={90}
height={20}
className="
h-4 w-auto
transition-all duration-500 ease-in-out
grayscale opacity-60
group-hover:grayscale-0 group-hover:opacity-100
"
unoptimized
/>
</div>
)}
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1">
{service.stack.map((tech) => (
<span
key={tech}
className="text-[10px] text-blue-500/70 uppercase tracking-widest font-bold"
>
{tech}
</span>
))}
</div>
</div>
<p className="text-neutral-400 text-sm leading-relaxed max-w-md">
{service.description}
</p>
{/* Conditional Actions */}
<div className="flex items-center gap-6 pt-4 border-t border-neutral-800/50">
{service.visibility === "public" && service.url ? (
<a
href={service.url}
target="_blank"
className="flex items-center gap-2 text-[10px] text-white hover:text-blue-400 transition-colors uppercase font-bold tracking-widest"
>
<Globe size={14} /> Visit Service
</a>
) : (
<div className="flex items-center gap-2 text-[10px] text-neutral-600 uppercase font-bold tracking-widest cursor-default">
<Lock size={12} className="text-blue-500/50" /> VPN
Encrypted
</div>
)}
{service.gitUrl && (
<a
href={service.gitUrl}
target="_blank"
className="group/git flex items-center gap-2 text-[10px] text-neutral-500 hover:text-white transition-colors uppercase font-bold tracking-widest bg-neutral-900/50 px-3 py-1.5 rounded-lg border border-neutral-800 hover:border-neutral-700"
>
<Image
src="/forgejo.svg"
alt="Forgejo"
width={12}
height={12}
className="opacity-50 group-hover/git:opacity-100 transition-opacity"
/>
{`Source`}
</a>
)}
</div>
</div>
</motion.section>
);
})}
</div>
</PageLayout>
);
}