Initial portfolio commit (Next.js + Docker + Gitea integration)

This commit is contained in:
2026-02-25 11:38:31 +01:00
commit 5b7fe3545e
40 changed files with 8919 additions and 0 deletions

5
app/_data/get-project.ts Normal file
View File

@@ -0,0 +1,5 @@
import { projects } from "./projects";
export function getProjectBySlug(slug: string) {
return projects.find((p) => p.slug === slug);
}

99
app/_data/projects.ts Normal file
View File

@@ -0,0 +1,99 @@
export type ProjectLink = { label: string; href: string };
export type ProjectSection = {
title: string;
content: string;
};
export type Project = {
slug: string;
title: string;
description: string;
tag: string;
href: string;
image: string;
year?: string;
stack?: string[];
status?: "Live" | "WIP" | "Archived";
links?: ProjectLink[];
sections?: ProjectSection[];
gallery?: string[];
};
export const projects: Project[] = [
{
slug: "unraid-docker",
title: "Unraid + Docker Homelab",
description:
"Cloudflare Tunnel → NPM → Docker services. Clean routing, access control, and observability-first layout.",
tag: "Infrastructure + UI",
href: "/projects/unraid-docker",
image: "/projects/unraid_docker.png",
year: "2026",
stack: ["Unraid", "Docker", "Cloudflare", "NPM"],
status: "WIP",
links: [
{ label: "Home", href: "/" },
{ label: "Lab", href: "/lab" },
{ label: "Projects", href: "/#projects" },
],
gallery: [
"/projects/unraid_docker.png",
"/projects/image_2026-02-21_112603143.png",
],
sections: [
{
title: "Goal",
content:
"A secure, cleanly routed homelab with no open router ports—Cloudflare Tunnel into Nginx Proxy Manager, backed by containerized services and a structured dashboard UI.",
},
{
title: "Architecture",
content:
"Cloudflare → Tunnel → NPM → service containers. The portfolio and dashboard live behind NPM; access lists protect sensitive routes.",
},
{
title: "Next",
content:
"Wire read-only Docker discovery via docker-socket-proxy, then implement the Lab dashboard: metrics row, service list, and topology visualization with Hide IP/Ports ON by default.",
},
],
},
{
slug: "macropad",
title: "Macropad Controller",
description:
"A tactile control surface for workflows: 12 macro keys, 4 encoders, and RGB feedback — paired with a desktop app for press/hold macros and per-app volume control.",
tag: "Embedded + Desktop",
href: "/projects/macropad",
image: "/projects/macropad.png",
year: "20252026",
stack: ["ESP32", "USB HID", "FastLED", "Python", "GUI", "EEPROM"],
status: "WIP",
links: [
{ label: "Home", href: "/" },
{ label: "Projects", href: "/#projects" },
],
gallery: [
"/projects/macropad.png",
"/projects/Model.png",
],
sections: [
{
title: "What it is",
content:
"A programmable macropad with physical controls and real-time RGB feedback. Its built for fast actions (macros), precise adjustments (encoders), and clear state visibility (LED bars/rings).",
},
{
title: "Why it exists",
content:
"Keyboard shortcuts are powerful but invisible. This gives you a physical interface where macros, holds, and volume states are obvious and consistent across apps.",
},
{
title: "How it works",
content:
"The desktop app assigns press/hold macros and target apps for volume control. The ESP32 executes input logic and drives LED animation synced to system state.",
},
],
},
];

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

106
app/globals.css Normal file
View File

@@ -0,0 +1,106 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ------------------------------------------------ */
/* Base Typography & Rendering Improvements */
/* ------------------------------------------------ */
html {
font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html, body {
max-width: 100%;
overflow-x: hidden;
}
/* ------------------------------------------------ */
/* Liquid Glass System */
/* ------------------------------------------------ */
.glass {
background: rgba(255, 255, 255, 0.62);
backdrop-filter: blur(22px) saturate(1.35);
-webkit-backdrop-filter: blur(22px) saturate(1.35);
border: 1px solid rgba(0, 0, 0, 0.07);
box-shadow:
0 22px 70px rgba(0, 0, 0, 0.08),
0 2px 12px rgba(0, 0, 0, 0.04);
}
.glass-strong {
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(26px) saturate(1.45);
-webkit-backdrop-filter: blur(26px) saturate(1.45);
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow:
0 28px 90px rgba(0, 0, 0, 0.10),
0 3px 16px rgba(0, 0, 0, 0.05);
}
/* ------------------------------------------------ */
/* Specular Highlight Overlay (Apple-like sheen) */
/* ------------------------------------------------ */
.specular {
position: relative;
overflow: hidden;
}
.specular::before {
content: "";
position: absolute;
inset: -40%;
background: radial-gradient(
circle at 30% 20%,
rgba(255, 255, 255, 0.65),
rgba(255, 255, 255, 0.00) 55%
);
transform: rotate(-8deg);
pointer-events: none;
}
.specular::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.18),
rgba(255, 255, 255, 0.00) 30%,
rgba(0, 0, 0, 0.02)
);
pointer-events: none;
}
/* Hide scrollbars for gallery */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* ------------------------------------------------ */
/* Optional Subtle Noise Layer (if used) */
/* ------------------------------------------------ */
.noise-overlay {
pointer-events: none;
position: absolute;
inset: 0;
opacity: 0.06;
mix-blend-mode: soft-light;
background-image: url("/noise.png");
}

39
app/lab/page.tsx Normal file
View File

@@ -0,0 +1,39 @@
export const dynamic = "force-dynamic";
export const revalidate = 0;
export default function LabPlaceholder() {
return (
<main className="min-h-screen bg-white relative">
<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>
<div className="relative mx-auto max-w-6xl px-6 py-10">
<h1 className="text-4xl md:text-5xl font-semibold tracking-tight text-black/90">
Lab
</h1>
<p className="mt-3 text-black/70 max-w-2xl leading-relaxed">
Dashboard shell comes next: metrics row + services list + topology, with
Hide IP/Ports default ON.
</p>
<div className="mt-8 rounded-3xl glass specular p-6">
<div className="text-sm font-semibold text-black/85">Status</div>
<div className="mt-2 text-sm text-black/70 leading-relaxed">
Coming online after UI + read-only container discovery is wired.
</div>
</div>
<div className="mt-10">
<a
className="rounded-3xl border border-black/15 px-5 py-3 text-sm font-medium hover:border-black/30 transition"
href="/"
>
Back
</a>
</div>
</div>
</main>
);
}

22
app/layout.tsx Normal file
View File

@@ -0,0 +1,22 @@
import "./globals.css";
import Navbar from "@/components/navbar";
export const metadata = {
title: "Brian De Winne",
description: "Portfolio + lab dashboard",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="overflow-x-hidden">
<body className="bg-white text-black antialiased overflow-x-hidden">
<Navbar />
{children}
</body>
</html>
);
}

29
app/page.tsx Normal file
View File

@@ -0,0 +1,29 @@
export const dynamic = "force-dynamic";
export const revalidate = 0;
import Hero from "@/components/hero";
import FeaturedProjects from "@/components/featured-projects";
import Footer from "@/components/footer";
export default function HomePage() {
return (
<main className="min-h-screen bg-white relative">
{/* Ambient “liquid” 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 className="pointer-events-none absolute inset-0 opacity-[0.06] mix-blend-soft-light"
style={{ backgroundImage: "public/noise.png" }} />
</div>
<div className="relative mx-auto max-w-6xl px-6 py-10">
<Hero />
<div className="mt-14" id="projects">
<FeaturedProjects />
</div>
<div className="mt-16">
<Footer />
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,275 @@
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>
);
}