137 lines
5.6 KiB
TypeScript
137 lines
5.6 KiB
TypeScript
"use client";
|
|
import { LAB_SERVICES } from "@/data/lab";
|
|
import { Lock, Box, 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 self hosted 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>
|
|
);
|
|
}
|