120 lines
3.9 KiB
TypeScript
120 lines
3.9 KiB
TypeScript
"use client";
|
|
import { motion } from "framer-motion";
|
|
import Link from "next/link";
|
|
import { Globe, Smartphone, ArrowLeft } from "lucide-react";
|
|
import { use } from "react";
|
|
|
|
const categories = {
|
|
web: {
|
|
title: "Web Systems",
|
|
icon: <Globe className="w-8 h-8 text-blue-400" />,
|
|
description: "Architecting scalable web applications and distributed systems.",
|
|
projects: [
|
|
{
|
|
name: "Ratoong",
|
|
detail: "Professional production platform.",
|
|
stack: ["Node.js", "PostgreSQL", "Caddy"]
|
|
},
|
|
{
|
|
name: "Datasaur",
|
|
detail: "Full-stack data science pipeline.",
|
|
stack: ["Python", "FastAPI", "Next.js"]
|
|
}
|
|
]
|
|
},
|
|
mobile: {
|
|
title: "Mobile Apps",
|
|
icon: <Smartphone className="w-8 h-8 text-purple-400" />,
|
|
description: "Building cross-platform experiences with Flutter and native integrations.",
|
|
projects: [
|
|
{
|
|
name: "Flutter App 1",
|
|
detail: "Active Development - Coming Soon",
|
|
stack: ["Flutter", "Dart", "Firebase"]
|
|
},
|
|
{
|
|
name: "Flutter App 2",
|
|
detail: "Internal R&D Prototype",
|
|
stack: ["Flutter", "Riverpod", "SQLite"]
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
export default function CategoryPage({ params }: { params: Promise<{ category: string }> }) {
|
|
|
|
|
|
const resolvedParams = use(params);
|
|
const category = resolvedParams.category;
|
|
|
|
const data = categories[category as keyof typeof categories];
|
|
|
|
if (!data) return <div className="p-24 text-white">Category not found.</div>;
|
|
|
|
|
|
if (!data) {
|
|
return (
|
|
<div className="p-24 text-white font-mono">
|
|
<h1 className="text-2xl mb-4">404: Category Not Found</h1>
|
|
<p className="text-neutral-500">Path: /projects/{category}</p>
|
|
<Link href="/" className="text-blue-400 underline mt-4 block">Return Home</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<main className="min-h-screen bg-[#0a0a0a] text-white p-8 md:p-24">
|
|
<Link href="/" className="flex items-center gap-2 text-neutral-500 hover:text-white transition-colors mb-12 font-mono text-xs">
|
|
<ArrowLeft size={14} /> BACK TO DASHBOARD
|
|
</Link>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="max-w-5xl mx-auto"
|
|
>
|
|
<div className="flex items-center gap-4 mb-6">
|
|
{data.icon}
|
|
<h1 className="text-5xl font-bold">{data.title}</h1>
|
|
</div>
|
|
<p className="text-xl text-neutral-400 max-w-2xl mb-16">{data.description}</p>
|
|
|
|
<div className="grid grid-cols-1 gap-8">
|
|
{data.projects.map((project, index) => (
|
|
<Link
|
|
key={project.name}
|
|
href={`/projects/${category}/${project.name.toLowerCase()}`}>
|
|
<motion.div
|
|
|
|
layout
|
|
initial={{ opacity: 0, x: -20 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{
|
|
delay: index * 0.1,
|
|
duration: 0.4,
|
|
ease: "easeOut"
|
|
}}
|
|
className="group p-8 rounded-3xl bg-neutral-900/50 border border-neutral-800 hover:border-neutral-700 transition-colors"
|
|
>
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h3 className="text-2xl font-semibold mb-2">{project.name}</h3>
|
|
<p className="text-neutral-500">{project.detail}</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{project.stack.map(s => (
|
|
<span key={s} className="text-[10px] font-mono bg-neutral-800 text-neutral-400 px-3 py-1 rounded-full uppercase">
|
|
{s}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
</main>
|
|
);
|
|
} |