123 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
}
|