276 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|