Files
portfolio/app/projects/[slug]/page.tsx

276 lines
11 KiB
TypeScript

import { notFound } from "next/navigation";
import { getProjectBySlug } from "@/app/_data/get-project";
import GalleryCarousel from "@/components/gallery-carousel";
import ScrollHero from "@/components/scroll-hero";
import { Reveal, Stagger, StaggerItem } from "@/components/reveal";
export const dynamic = "force-dynamic";
export const revalidate = 0;
function Badge({ children }: { children: React.ReactNode }) {
return (
<span className="inline-flex items-center rounded-full border border-black/10 bg-white/60 px-3 py-1 text-xs text-black/65">
{children}
</span>
);
}
function FeatureTile({ title, desc }: { title: string; desc: string }) {
return (
<div className="rounded-3xl glass specular p-5">
<div className="text-sm font-semibold text-black/85">{title}</div>
<div className="mt-2 text-sm text-black/75 leading-relaxed">{desc}</div>
</div>
);
}
export default async function ProjectPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const project = getProjectBySlug(slug);
if (!project) return notFound();
return (
<main className="min-h-screen bg-white relative">
{/* ambient light */}
<div className="pointer-events-none absolute inset-0">
<div className="absolute -top-40 -left-40 h-[520px] w-[520px] rounded-full bg-black/5 blur-3xl" />
<div className="absolute -bottom-40 -right-40 h-[520px] w-[520px] rounded-full bg-black/5 blur-3xl" />
</div>
{/* optional noise (if you generated noise.png) */}
<div className="noise-overlay" />
<div className="relative mx-auto max-w-6xl px-6 py-10">
{/* breadcrumbs / links */}
<Reveal>
<div className="flex flex-wrap items-center gap-2">
{(project.links ?? [{ label: "Home", href: "/" }]).map((l) => (
<a
key={l.href}
href={l.href}
className="text-sm text-black/60 hover:text-black transition"
>
{l.label}
</a>
))}
<span className="text-sm text-black/40">/</span>
<span className="text-sm text-black/70">{project.title}</span>
</div>
</Reveal>
{/* header */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
<Reveal delay={0.02}>
<div>
<div className="text-sm text-black/60">{project.tag}</div>
<h1 className="mt-3 text-4xl md:text-5xl font-semibold tracking-tight text-black/90 leading-[1.05]">
{project.title}
</h1>
<p className="mt-4 text-base md:text-lg text-black/75 leading-relaxed max-w-xl">
{project.description}
</p>
<div className="mt-6 flex flex-wrap gap-2">
{project.status && <Badge>{project.status}</Badge>}
{project.year && <Badge>{project.year}</Badge>}
{(project.stack ?? []).map((s) => (
<Badge key={s}>{s}</Badge>
))}
</div>
</div>
</Reveal>
{/* scroll-reactive hero */}
<Reveal delay={0.05}>
<ScrollHero src={project.image} alt={project.title} />
</Reveal>
</div>
{/* centered carousel */}
<Reveal delay={0.05}>
<GalleryCarousel
images={project.gallery ?? []}
title="Gallery"
autoPlay
intervalMs={4500}
/>
</Reveal>
{/* sections */}
<Stagger>
<div className="mt-10 grid grid-cols-1 lg:grid-cols-3 gap-5">
{(project.sections ?? []).map((s) => (
<StaggerItem key={s.title}>
<section className="rounded-3xl glass specular p-6">
<h2 className="text-sm font-semibold text-black/85">
{s.title}
</h2>
<p className="mt-3 text-sm text-black/75 leading-relaxed">
{s.content}
</p>
</section>
</StaggerItem>
))}
</div>
</Stagger>
{/* macropad enhancements */}
{project.slug === "macropad" && (
<div className="mt-10">
<Reveal>
<h2 className="text-xl font-semibold tracking-tight text-black/90">
Key capabilities
</h2>
<p className="mt-2 text-sm text-black/70 leading-relaxed max-w-2xl">
Designed around muscle memory: fast macros, precise control, and
clear feedback.
</p>
</Reveal>
<Stagger>
<div className="mt-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
<StaggerItem>
<FeatureTile
title="Press vs Hold macros"
desc="Assign separate actions for tap and hold, with a configurable threshold in milliseconds."
/>
</StaggerItem>
<StaggerItem>
<FeatureTile
title="Per-app volume control"
desc="Encoders adjust volume for selected programs, and the LED bars reflect the current level."
/>
</StaggerItem>
<StaggerItem>
<FeatureTile
title="LED state syncing"
desc="RGB strips and encoder rings display color + fill based on system state and app selection."
/>
</StaggerItem>
<StaggerItem>
<FeatureTile
title="Persistent settings"
desc="Color profiles and device preferences persist across reboots using EEPROM."
/>
</StaggerItem>
<StaggerItem>
<FeatureTile
title="Macro recording UI"
desc="Record key sequences automatically or enter them manually, then bind to any button."
/>
</StaggerItem>
<StaggerItem>
<FeatureTile
title="Profiles"
desc="Switch layouts per workflow. Export/import profiles as JSON."
/>
</StaggerItem>
</div>
</Stagger>
<Reveal delay={0.05}>
<div className="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-5">
{/* Roadmap */}
<div className="rounded-3xl glass-strong specular p-6">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-semibold text-black/85">Roadmap</div>
<span className="text-xs text-black/55">Last updated: Feb 2026</span>
</div>
<div className="mt-5 space-y-4">
{[
{
title: "WIP (now)",
desc: "Macro editor, press/hold mapping, LED syncing, core firmware stability.",
done: true,
},
{
title: "Beta",
desc: "Profiles + export/import, device discovery, per-app volume routing polish.",
done: false,
},
{
title: "v1",
desc: "Installer packaging, cross-platform hotkey backend, full documentation + demo video.",
done: false,
},
].map((item) => (
<div
key={item.title}
className="flex items-start gap-3 rounded-3xl border border-black/10 bg-white/60 p-4"
>
<div
className={[
"mt-0.5 h-3 w-3 rounded-full border",
item.done
? "bg-black/55 border-black/25"
: "bg-white/70 border-black/15",
].join(" ")}
/>
<div>
<div className="text-sm font-semibold text-black/85">
{item.title}
</div>
<div className="mt-1 text-sm text-black/70 leading-relaxed">
{item.desc}
</div>
</div>
</div>
))}
</div>
</div>
{/* Architecture */}
<div className="rounded-3xl glass-strong specular p-6">
<div className="text-sm font-semibold text-black/85">Architecture</div>
<div className="mt-4 grid grid-cols-1 gap-4 text-sm text-black/75">
<div className="rounded-3xl border border-black/10 bg-white/60 p-4">
<div className="text-xs text-black/60">ESP32 firmware</div>
<ul className="mt-2 list-disc pl-5 space-y-1 text-black/75">
<li>Encoder + button input scanning</li>
<li>Hold threshold + action dispatch</li>
<li>FastLED strips + ring feedback</li>
<li>EEPROM persistence (colors/settings)</li>
</ul>
</div>
<div className="rounded-3xl border border-black/10 bg-white/60 p-4">
<div className="text-xs text-black/60">Desktop app</div>
<ul className="mt-2 list-disc pl-5 space-y-1 text-black/75">
<li>Macro recording + manual entry</li>
<li>Per-app volume selection + control</li>
<li>Profiles (save/load/export JSON)</li>
<li>Sync state LEDs (color + fill)</li>
</ul>
</div>
</div>
</div>
</div>
</Reveal>
</div>
)}
{/* back */}
<Reveal delay={0.04}>
<div className="mt-10">
<a
href="/#projects"
className="inline-flex items-center rounded-3xl border border-black/15 bg-white/60 px-5 py-3 text-sm font-medium text-black/80 hover:text-black hover:border-black/30 transition"
>
Back to projects
</a>
</div>
</Reveal>
</div>
</main>
);
}