portfolio/app/projects/[category]/page.tsx
2026-02-02 13:40:02 +01:00

123 lines
3.8 KiB
TypeScript

"use client";
import { use } from "react";
import { motion, Variants } from "framer-motion";
import Link from "next/link";
import { Globe, Smartphone, Server } from "lucide-react";
import { PROJECT_REGISTRY } from "@/data/projects";
import PageLayout from "@/components/PageLayout";
const CATEGORY_META = {
web: {
title: "Web Systems",
icon: <Globe className="w-8 h-8 text-blue-400" />,
description:
"Architecting scalable web applications and distributed systems.",
},
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.",
},
};
// 1. Cast the container variants
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
// 2. Cast the item variants
const itemVariants: Variants = {
hidden: { opacity: 0, x: -20 },
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.4,
ease: "easeOut", // TypeScript now knows this is a valid Easing 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 filteredProjects = PROJECT_REGISTRY.filter(
(p) => p.category === category,
);
if (!meta) return <PageLayout backLink="/">Sector not found.</PageLayout>;
return (
<PageLayout backLink="/" maxWidth="5xl">
<div className="mb-16">
<h1 className="flex items-center gap-4 text-5xl font-bold tracking-tighter mb-6">
{meta.icon} {meta.title}
</h1>
<p className="text-xl text-neutral-400 max-w-2xl leading-relaxed">
{meta.description}
</p>
</div>
{/* 2. The container manages the entrance of all children */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid grid-cols-1 gap-6"
>
{filteredProjects.map((project) => (
<Link
key={project.slug}
href={`/projects/${category}/${project.slug}`}
className="block"
>
<motion.div
layout
variants={itemVariants}
whileHover={{ x: 8 }}
className="group p-8 rounded-3xl bg-neutral-900/40 border border-neutral-800 hover:border-neutral-700 transition-colors"
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h3 className="text-2xl font-bold mb-2 group-hover:text-blue-400 transition-colors">
{project.title}
</h3>
<p className="text-neutral-500 text-sm max-w-xl">
{project.description}
</p>
</div>
<div className="flex flex-wrap gap-2 md:justify-end">
{project.stack.map((tech) => (
<span
key={tech}
className="text-[9px] font-mono bg-neutral-800 text-neutral-400 px-3 py-1 rounded-full uppercase tracking-widest border border-neutral-800"
>
{tech}
</span>
))}
</div>
</div>
</motion.div>
</Link>
))}
</motion.div>
</PageLayout>
);
}