86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { motion } from "framer-motion";
|
|
import { projects } from "@/app/_data/projects";
|
|
|
|
|
|
function Badge({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<span className="inline-flex items-center rounded-full border border-black/10 bg-white/60 px-2.5 py-1 text-[11px] text-black/65">
|
|
{children}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export default function FeaturedProjects() {
|
|
return (
|
|
<section>
|
|
<div className="flex items-end justify-between gap-6">
|
|
<div>
|
|
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight text-black/90">
|
|
Featured projects
|
|
</h2>
|
|
<p className="mt-2 text-black/75 max-w-2xl leading-relaxed">
|
|
Infrastructure, software, and lab-adjacent builds — minimal UI with
|
|
real systems behind it.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-7 grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
{projects.map((p, i) => (
|
|
<motion.a
|
|
key={p.title}
|
|
href={p.href}
|
|
initial={{ opacity: 0, y: 12 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, amount: 0.35 }}
|
|
transition={{ duration: 0.45, delay: i * 0.05 }}
|
|
className="group rounded-3xl glass specular overflow-hidden transition will-change-transform hover:translate-y-[-2px]"
|
|
>
|
|
{/* image */}
|
|
<div className="relative h-56 w-full">
|
|
<Image
|
|
src={p.image}
|
|
alt={p.title}
|
|
fill
|
|
className="object-cover transition duration-700 ease-out group-hover:scale-[1.03]"
|
|
priority={i === 0}
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-black/0 to-transparent pointer-events-none" />
|
|
</div>
|
|
|
|
{/* content */}
|
|
<div className="p-5">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="text-xs text-black/60">{p.tag}</div>
|
|
{p.status && <Badge>{p.status}</Badge>}
|
|
</div>
|
|
|
|
<div className="mt-2 text-xl font-semibold tracking-tight text-black/90">
|
|
{p.title}
|
|
</div>
|
|
|
|
<div className="mt-2 text-sm text-black/75 leading-relaxed">
|
|
{p.description}
|
|
</div>
|
|
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
{p.year && <Badge>{p.year}</Badge>}
|
|
{(p.stack ?? []).slice(0, 4).map((s) => (
|
|
<Badge key={s}>{s}</Badge>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-5 text-sm font-medium text-black/70 group-hover:text-black/90 transition">
|
|
View →
|
|
</div>
|
|
</div>
|
|
</motion.a>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|