This commit is contained in:
Mike Karl 2026-04-22 15:40:44 +02:00
parent 417f2441f0
commit 0a1c4702f7
63 changed files with 3515 additions and 972 deletions

1
.gitignore vendored
View file

@ -16,6 +16,7 @@
# next.js # next.js
/.next/ /.next/
/out/ /out/
/dist/
# production # production
/build /build

View file

@ -1,90 +0,0 @@
### 1. Home / Landing
**Goal:** instant context
* Name + role
*“Software Developer focused on X”*
* 1-line value statement
*“I build reliable backend systems and clean UIs.”*
* Primary CTA: **View Projects** or **GitHub**
---
### 2. Projects (most important page)
**This is where hiring decisions start.**
For each project:
* **What it is** (1 sentence)
* **Tech stack**
* **Your role** (solo / team / size)
* **Key challenges & decisions**
* **What you’d improve next**
Examples:
* “Self-hosted document management system (Docker, Python, PostgreSQL)”
* “Matrix server setup & federation experiments”
* “WPF app solving UI threading issues”
👉 3–5 *good* projects beat 10 shallow ones.
---
### 3. About (technical, not fluffy)
Think “senior dev intro”, not life story.
Include:
* What kind of problems you enjoy solving
* How you approach learning & debugging
* What environments you work best in
Keep it to ~2 short paragraphs.
---
### 4. Skills / Tech Stack
**Grouped, not buzzword soup.**
Example:
* **Languages:** …
* **Frameworks:** …
* **Infrastructure / DevOps:** Docker, Linux, CI/CD
* **Databases:** …
* **Concepts:** REST, async, threading, security basics
Bonus points:
* “Comfortable with” vs “Familiar with”
---
### 5. Experience
* Jobs, freelance, serious hobby projects
* Focus on **impact**, not duties
Example:
> “Maintained and extended internal tooling used by X users”
---
### 6. Resume (PDF)
* One click download
* Matches the portfolio (no contradictions)
---
### 7. Contact
* Email
* GitHub
* LinkedIn
* (Optional) location / timezone

View file

@ -1,56 +0,0 @@
import Link from "next/link";
export default function Page() {
return (
<main className="min-h-screen flex items-center justify-center flex-col pt-24">
<h1 className="text-3xl font-bold mb-12">Über mich</h1>
<section className="md:px-24 px-8 py-6 w-full text-justify">
<h2 className="text-2xl font-semibold mb-1">Softwareentwickler:</h2>
<p>
Bei der Softwareentwicklung ist es mein Anspruch, ein System zu bauen, das nicht einfach nur
funktioniert, sondern robust, flexibel und zukunftorientiert ist. Mir ist wichtig sicherzustellen,
das Features auf einem soliden Fundament aufbauen. Dafür bleibe ich immer am Ball, um neue Ansätze
zu erlernen und die Trends der Branche im Auge zu behalten. Ich verbinde meine Expertise in
verschiedenen Teilgebieten der Informatik um eine sauber integrierte Lösung für die vor mir
liegenden Probleme zu finden.
<br />
Ich arbeite strukturiert und betrachte das große Ganze, kann mich allerdings auch an Details festbeißen,
sollte der Bedarf bestehen.
<br />
Mein Ziel ist es Software zu bauen, die dem Nutzer aus dem Weg geht und eine Umgebung zu schaffen,
in der sowohl Endnutzer als auch Entwickler so einfach wie möglich vorran kommen können.
<br />
Dies gilt sowohl für meine professionelle Arbeit als auch für side projects.
<br />
<Link href="/projects" className="inline-flex items-center border rounded-lg p-1.5 mt-4 w-fit mr-3 hover:scale-105 active:rotate-2 px-4">
Zu meinen Projekten
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" className="ml-2 w-6 h-6 fill-white"><path d="M384 64C366.3 64 352 78.3 352 96C352 113.7 366.3 128 384 128L466.7 128L265.3 329.4C252.8 341.9 252.8 362.2 265.3 374.7C277.8 387.2 298.1 387.2 310.6 374.7L512 173.3L512 256C512 273.7 526.3 288 544 288C561.7 288 576 273.7 576 256L576 96C576 78.3 561.7 64 544 64L384 64zM144 160C99.8 160 64 195.8 64 240L64 496C64 540.2 99.8 576 144 576L400 576C444.2 576 480 540.2 480 496L480 416C480 398.3 465.7 384 448 384C430.3 384 416 398.3 416 416L416 496C416 504.8 408.8 512 400 512L144 512C135.2 512 128 504.8 128 496L128 240C128 231.2 135.2 224 144 224L224 224C241.7 224 256 209.7 256 192C256 174.3 241.7 160 224 160L144 160z" /></svg>
</Link>
</p>
</section>
<section className="md:px-24 px-8 py-6 w-full text-justify">
<h2 className="text-2xl font-semibold mb-1">Motorradfahrer:</h2>
<p>
Der 1. April ist immer einer der besten Tage im Jahr. Die neue Motorradsaison beginnt. Als Kind fuhr
ich oft bei anderen mit und wusste immer schon, dass ich einmal den Führerschein machen würde.
Nach vielen Jahren, in denen ich weder Zeit noch Geld hatte, machte ich 2023 den A-Klasse Schein.
Mit meiner geliebten Kawa fahre ich häufig meine Haustrecke über den Arber. Seit letztem Jahr auch
mit Sozia.
</p>
</section>
<section className="md:px-24 px-8 py-6 w-full text-justify">
<h2 className="text-2xl font-semibold mb-1">Astrofotograf:</h2>
<p>
Was als P-Seminar während der Oberstufe begannt, entwickelte sich über die Jahre in ein richtiges
Hobby. Der Weltraum ist so ein unbegreifliches Konzept, dass es fast surreal erscheint, wie viel
eine einfache Kamera doch einfangen kann. Viele Nächte verbringe ich mit Stativ und DSLR draußen
auf einer weiten Wiese, um am nächsten Tag aufgeregt am PC die Ergebnisse zu betrachten. Mein
Lieblingsmotiv ist der Orion Nebel, da er selbst mit dem bloßen Auge erkennbar ist. Ein so tolles
Motiv, das auf das untrainierte Auge nur wie ein weiterer Stern aussieht. Für die nächsten Jahre
habe ich mir vorgenommen, auf ein Teleskop mit Star-Tracker aufzurüsten und so noch viele weitere
Beobachtungsziele in meinen Katalog aufzunehmen.
</p>
</section>
</main>
);
}

View file

@ -1,9 +0,0 @@
export default function Header({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <header className="fixed top-0 flex justify-between w-full z-10 backdrop-blur-sm">
{children}
</header>
}

View file

@ -1,25 +0,0 @@
'use client'
import Link from "next/link";
import { usePathname } from "next/navigation";
export default function NavItem({
children,
href
}: Readonly<{
children: React.ReactNode;
href: string;
}>) {
const highlighted = usePathname() == href;
return (
<Link
href={href}
className={`group relative text-lg mx-4 flex font-semibold hover:scale-110 active:rotate-4`}
>
<span>{children}</span>
<span
className={`absolute left-0 bottom-0 h-[2px] w-full bg-current transform transition-transform duration-200 origin-left group-hover:scale-x-100 ${highlighted ? 'scale-x-100 scale-y-100' : 'scale-x-0 scale-y-50'}`}
aria-hidden="true"
/>
</Link>
);
}

View file

@ -1,9 +0,0 @@
export default function Navbar({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <nav className="p-5 flex justify-between">
{children}
</nav>
}

View file

@ -1,9 +0,0 @@
export default function Section({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <section>
{children}
</section>
}

View file

@ -1,9 +0,0 @@
export default function SectionContent({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <div>
{children}
</div>
}

View file

@ -1,9 +0,0 @@
export default function SectionTitle({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <div>
{children}
</div>
}

View file

@ -1,23 +0,0 @@
import Link from "next/link";
export default function Page() {
return (
<main className="min-h-screen flex items-center justify-center flex-col text-center">
<section className="w-72">
<h1 className="text-3xl font-bold mb-4">Kontakt</h1>
<Link href="https://github.com/GoranSalzmann" className="justify-between inline-flex items-center border rounded-lg p-1.5 mt-4 w-full hover:scale-105 active:rotate-2 px-4">Github
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="ml-2 w-8 h-8 fill-white"><path d="M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM252.8 8c-138.7 0-244.8 105.3-244.8 244 0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1 100-33.2 167.8-128.1 167.8-239 0-138.7-112.5-244-251.2-244zM105.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" /></svg>
</Link>
<Link href="https://www.linkedin.com/in/mike-karl-a58925381" className="justify-between inline-flex items-center border rounded-lg p-1.5 mt-4 w-full hover:scale-105 active:rotate-2 px-4">LinkedIn
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" className="ml-2 w-8 h-8 fill-white"><path d="M416 32L31.9 32C14.3 32 0 46.5 0 64.3L0 447.7C0 465.5 14.3 480 31.9 480L416 480c17.6 0 32-14.5 32-32.3l0-383.4C448 46.5 433.6 32 416 32zM135.4 416l-66.4 0 0-213.8 66.5 0 0 213.8-.1 0zM102.2 96a38.5 38.5 0 1 1 0 77 38.5 38.5 0 1 1 0-77zM384.3 416l-66.4 0 0-104c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9l0 105.8-66.4 0 0-213.8 63.7 0 0 29.2 .9 0c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9l0 117.2z" /></svg>
</Link>
<Link href="mailto:karl.mike2002@gmail.com" className="justify-between inline-flex items-center border rounded-lg p-1.5 mt-4 w-full hover:scale-105 active:rotate-2 px-4">karl.mike2002@gmail.com
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" className="ml-2 w-8 h-8 fill-white"><path d="M320 128C214 128 128 214 128 320C128 426 214 512 320 512C337.7 512 352 526.3 352 544C352 561.7 337.7 576 320 576C178.6 576 64 461.4 64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320L576 352C576 405 533 448 480 448C450.7 448 424.4 434.8 406.8 414.1C384 435.1 353.5 448 320 448C249.3 448 192 390.7 192 320C192 249.3 249.3 192 320 192C347.9 192 373.7 200.9 394.7 216.1C400.4 211.1 407.8 208 416 208C433.7 208 448 222.3 448 240L448 352C448 369.7 462.3 384 480 384C497.7 384 512 369.7 512 352L512 320C512 214 426 128 320 128zM384 320C384 284.7 355.3 256 320 256C284.7 256 256 284.7 256 320C256 355.3 284.7 384 320 384C355.3 384 384 355.3 384 320z" /></svg>
</Link>
<Link href="tel:015122597580" className="justify-between inline-flex items-center border rounded-lg p-1.5 mt-4 w-full hover:scale-105 active:rotate-2 px-4">01512 2597580
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" className="ml-2 w-8 h-8 fill-white"><path d="M224.2 89C216.3 70.1 195.7 60.1 176.1 65.4L170.6 66.9C106 84.5 50.8 147.1 66.9 223.3C104 398.3 241.7 536 416.7 573.1C493 589.3 555.5 534 573.1 469.4L574.6 463.9C580 444.2 569.9 423.6 551.1 415.8L453.8 375.3C437.3 368.4 418.2 373.2 406.8 387.1L368.2 434.3C297.9 399.4 241.3 341 208.8 269.3L253 233.3C266.9 222 271.6 202.9 264.8 186.3L224.2 89z" /></svg>
</Link>
</section>
</main>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,27 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap');
@import "tailwindcss";
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
font-family: "Space Grotesk", sans-serif;
color: #fff;
background: #000;
}
.bg{
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: -999;
background:
radial-gradient(circle at 0% 0%, #da0202 0%, transparent 50%),
radial-gradient(circle at 25% -5%, #da0202 0%, transparent 40%),
radial-gradient(circle at 100% 100%, #da0202 0%, transparent 40%),
#000
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

View file

@ -1,42 +0,0 @@
import type { Metadata } from "next";
import "./globals.css";
import Navbar from "./components/Navbar";
import NavItem from "./components/NavItem";
import Header from "./components/Header";
import Image from "next/image";
import logo from "./images/logo.png"
import Link from "next/link";
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Analytics } from '@vercel/analytics/next';
export const metadata: Metadata = {
title: "Portfolio - Mike Karl",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="de">
<body>
<Header>
<Navbar>
<NavItem href="/">Home</NavItem>
<NavItem href="/projects">Projekte</NavItem>
<NavItem href="/about">About</NavItem>
<NavItem href="/contact">Kontakt</NavItem>
</Navbar>
<Link href={"/"}>
<Image alt="logo.png" className="mx-4" src={logo} width={70} height={70} />
</Link>
</Header>
{children}
<div className="bg" />
<SpeedInsights />
<Analytics />
</body>
</html>
);
}

View file

@ -1,21 +0,0 @@
import Link from "next/link";
export default function NotFound() {
return (
<div className="top-80 flex items-center justify-center px-4 absolute w-screen">
<div className="text-center">
<h1 className="text-6xl md:text-8xl font-bold mb-4">404</h1>
<h2 className="text-2xl md:text-4xl font-semibold mb-4">Page Not Found</h2>
<p className="text-gray-400 mb-8 text-lg">
Sorry, the page you're looking for doesn't exist.
</p>
<Link
href="/"
className="inline-block px-6 py-3 bg-white text-black font-semibold rounded hover:bg-gray-200 transition-colors"
>
Back to Home
</Link>
</div>
</div>
);
}

View file

@ -1,44 +0,0 @@
'use client'
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import { useEffect, useState } from "react";
export default function Home() {
const jobs = ["Software", "Fullstack", "Web", "System", "Rust"];
const [job, setJob] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setJob((prev) => (prev + 1) % jobs.length);
}, 3000);
return () => clearInterval(interval);
}, []);
return (
<main className="min-h-screen flex items-center justify-center flex-col pt-12">
<section className="mx-auto w-fit md:w-full md:pl-36">
<h2 className="text-7xl/12 font-light">Mike</h2>
<h1 className="text-9xl font-bold">Karl</h1>
<AnimatePresence mode="wait">
<motion.h3
key={jobs[job]}
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -20, opacity: 0 }}
transition={{ duration: 0.5 }}
className="relative text-4xl my-2"
>
{jobs[job]} Entwickler
</motion.h3>
</AnimatePresence>
<Link href="https://github.com/GoranSalzmann" className="inline-flex items-center border rounded-lg p-1.5 mt-4 w-fit mr-3 hover:scale-105 active:rotate-2 px-4">Mein Github
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="ml-2 w-8 h-8 fill-white"><path d="M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM252.8 8c-138.7 0-244.8 105.3-244.8 244 0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1 100-33.2 167.8-128.1 167.8-239 0-138.7-112.5-244-251.2-244zM105.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" /></svg>
</Link>
<Link href="https://www.linkedin.com/in/mike-karl-a58925381" className="inline-flex items-center border rounded-lg p-1.5 mt-4 w-fit mr-3 hover:scale-105 active:rotate-2 px-4">Mein LinkedIn
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" className="ml-2 w-8 h-8 fill-white"><path d="M416 32L31.9 32C14.3 32 0 46.5 0 64.3L0 447.7C0 465.5 14.3 480 31.9 480L416 480c17.6 0 32-14.5 32-32.3l0-383.4C448 46.5 433.6 32 416 32zM135.4 416l-66.4 0 0-213.8 66.5 0 0 213.8-.1 0zM102.2 96a38.5 38.5 0 1 1 0 77 38.5 38.5 0 1 1 0-77zM384.3 416l-66.4 0 0-104c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9l0 105.8-66.4 0 0-213.8 63.7 0 0 29.2 .9 0c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9l0 117.2z" /></svg>
</Link>
</section>
</main>
);
}

View file

@ -1,7 +0,0 @@
export default function Page() {
return (
<main className="min-h-screen flex items-center justify-center">
<h1 className="text-3xl font-bold">Projects</h1>
</main>
);
}

View file

@ -1,7 +1,11 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ output: "export",
distDir: "dist",
images: {
unoptimized: true,
},
}; };
export default nextConfig; export default nextConfig;

1854
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,26 +1,31 @@
{ {
"name": "portfolio", "name": "portfolio",
"version": "0.1.0",
"private": true, "private": true,
"version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev",
"build": "next build --turbopack", "build": "next build",
"start": "next start" "start": "next start",
"lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@vercel/analytics": "^1.6.1", "clsx": "^2.1.1",
"@vercel/speed-insights": "^1.3.1", "gsap": "^3.15.0",
"framer-motion": "^12.33.0", "lucide-react": "^1.8.0",
"next": "^15.5.12", "next": "^16.2.4",
"react": "19.1.0", "react": "^19.2.5",
"react-dom": "19.1.0" "react-dom": "^19.2.5",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4.2.4",
"@types/node": "^20", "@types/node": "^24.12.2",
"@types/react": "^19", "@types/react": "^19.2.14",
"@types/react-dom": "^19", "@types/react-dom": "^19.2.3",
"tailwindcss": "^4", "eslint": "^10.2.1",
"typescript": "^5" "postcss": "^8.5.0",
"typescript": "~6.0.2"
} }
} }

View file

@ -1,5 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = { const config = {
plugins: ["@tailwindcss/postcss"], plugins: {
"@tailwindcss/postcss": {},
},
}; };
export default config; export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

4
public/favicon.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="#0f0e0c"/>
<text x="50" y="50" font-family="Arial, Helvetica, sans-serif" font-size="44" font-weight="700" fill="#e86101" text-anchor="middle" dominant-baseline="central">MK</text>
</svg>

After

Width:  |  Height:  |  Size: 292 B

133
src/app/about/page.tsx Normal file
View file

@ -0,0 +1,133 @@
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { values, aboutPage } from "@/data/about";
import { site } from "@/data/site";
import { Icon } from "@/lib/icons";
export default function AboutPage() {
return (
<main className="min-h-screen bg-deep-space">
{/* Header */}
<section className="pt-32 pb-20 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto space-y-6">
<p className="section-label">{aboutPage.label}</p>
<h1
className="font-display font-bold text-text-primary leading-[0.9] tracking-tight"
style={{ fontSize: "clamp(2.5rem, 8vw, 5rem)" }}
>
{aboutPage.pageTitle}
</h1>
<p className="text-text-secondary max-w-2xl text-lg leading-relaxed">
{aboutPage.pageSubtitle}
</p>
</div>
</section>
{/* Content */}
<section className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-16">
<div className="space-y-8">
<h2 className="font-display font-semibold text-text-primary text-2xl tracking-tight">
{aboutPage.philosophyTitle}
</h2>
<p className="text-text-secondary leading-relaxed">
{aboutPage.philosophyText[0]}
</p>
<p className="text-text-secondary leading-relaxed">
{aboutPage.philosophyText[1]}
</p>
<h2 className="font-display font-semibold text-text-primary text-2xl tracking-tight pt-8">
{aboutPage.backgroundTitle}
</h2>
<p className="text-text-secondary leading-relaxed">
{aboutPage.backgroundText[0]}
</p>
<p className="text-text-secondary leading-relaxed">
{aboutPage.backgroundText[1]}
</p>
<div className="pt-4">
<Link
href="/hobbies"
className="inline-flex items-center gap-2 text-amber hover:text-amber-hover transition-colors group"
>
<span className="text-link">
{aboutPage.linkText}
</span>
<ArrowRight
size={18}
className="group-hover:translate-x-1 transition-transform"
/>
</Link>
</div>
</div>
<div className="space-y-8">
<div className="border border-warm-grey p-6 space-y-4">
<h3 className="font-display font-medium text-text-primary">
{aboutPage.sidebarFocusTitle}
</h3>
<ul className="space-y-2 text-text-secondary text-sm">
{aboutPage.sidebarFocusItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
<div className="border border-warm-grey p-6 space-y-4">
<h3 className="font-display font-medium text-text-primary">
{aboutPage.sidebarLocationTitle}
</h3>
<p className="text-text-secondary text-sm">
{site.location}
</p>
</div>
<div className="border border-warm-grey p-6 space-y-4">
<h3 className="font-display font-medium text-text-primary">
{aboutPage.sidebarHobbiesTitle}
</h3>
<ul className="space-y-2 text-text-secondary text-sm">
{aboutPage.sidebarHobbiesItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<Link
href="/hobbies"
className="text-link text-amber text-sm"
>
{aboutPage.sidebarHobbiesLink}
</Link>
</div>
</div>
</div>
</section>
{/* Values */}
<section className="py-20 md:py-32 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto">
<p className="section-label mb-12">{aboutPage.valuesTitle}</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
{values.map((value) => (
<div key={value.title} className="space-y-6">
<div className="w-10 h-10 flex items-center justify-center border border-warm-grey">
<Icon name={value.icon} size={20} className="text-amber" />
</div>
<h3 className="font-display font-semibold text-text-primary text-2xl tracking-tight">
{value.title}
</h3>
<p className="text-text-secondary leading-relaxed">
{value.text}
</p>
<p className="font-display text-text-primary italic border-l-2 border-amber pl-4">
&ldquo;{value.quote}&rdquo;
</p>
</div>
))}
</div>
</div>
</section>
</main>
);
}

93
src/app/contact/page.tsx Normal file
View file

@ -0,0 +1,93 @@
import { site } from "@/data/site";
import { contactContent, contactPage } from "@/data/contact";
import { Mail } from "lucide-react";
export default function ContactPage() {
return (
<main className="min-h-screen bg-deep-space">
{/* Header */}
<section className="pt-32 pb-20 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto space-y-6">
<p className="section-label">{contactPage.label}</p>
<h1
className="font-display font-bold text-text-primary leading-[0.9] tracking-tight"
style={{ fontSize: "clamp(2.5rem, 8vw, 5rem)" }}
>
{contactContent.heading}
</h1>
<p className="text-text-secondary max-w-2xl text-lg leading-relaxed">
{contactPage.pageSubtitle}
</p>
</div>
</section>
{/* Content */}
<section className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-16">
<div className="space-y-8">
<h2 className="font-display font-semibold text-text-primary text-2xl tracking-tight">
{contactPage.contentTitle}
</h2>
<p className="text-text-secondary leading-relaxed">
{contactContent.text}
</p>
<p className="text-text-secondary leading-relaxed">
{contactPage.contentPlaceholder1}
</p>
<p className="text-text-secondary leading-relaxed">
{contactPage.contentPlaceholder2}
</p>
<div className="pt-4">
<a
href={`mailto:${site.email}`}
className="inline-flex items-center gap-3 px-6 py-3 bg-amber text-deep-space font-display font-medium hover:bg-amber-hover transition-colors"
>
<Mail size={18} />
{contactContent.cta}
</a>
</div>
</div>
<div className="space-y-8">
<div className="border border-warm-grey p-6 space-y-4">
<h3 className="font-display font-medium text-text-primary">
{contactPage.sidebarEmailTitle}
</h3>
<a
href={`mailto:${site.email}`}
className="text-link text-amber font-mono text-sm"
>
{site.email}
</a>
</div>
<div className="border border-warm-grey p-6 space-y-4">
<h3 className="font-display font-medium text-text-primary">
{contactPage.sidebarSocialTitle}
</h3>
<div className="space-y-2 text-sm">
<a
href={site.github}
target="_blank"
rel="noopener noreferrer"
className="block text-text-secondary hover:text-amber transition-colors"
>
{contactPage.sidebarSocialGithub}
</a>
<a
href={site.linkedin}
target="_blank"
rel="noopener noreferrer"
className="block text-text-secondary hover:text-amber transition-colors"
>
{contactPage.sidebarSocialLinkedin}
</a>
</div>
</div>
</div>
</div>
</section>
</main>
);
}

105
src/app/globals.css Normal file
View file

@ -0,0 +1,105 @@
@import "tailwindcss";
@theme {
--color-deep-space: #0f0e0c;
--color-charcoal: #1a1917;
--color-warm-grey: #2a2926;
--color-text-primary: #e8e6e3;
--color-text-secondary: #8a8680;
--color-amber: #e86101;
--color-amber-hover: #FF751A;
--color-amber-10: #e861011a;
--color-amber-20: #e8610133;
--color-amber-30: #e861014d;
--font-display: var(--font-space-grotesk);
--font-body: var(--font-inter);
--font-mono: var(--font-ibm-plex-mono);
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-body);
background-color: var(--color-deep-space);
color: var(--color-text-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::selection {
background-color: var(--color-amber-30);
color: var(--color-text-primary);
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-deep-space);
}
::-webkit-scrollbar-thumb {
background: var(--color-warm-grey);
border-radius: 0;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
/* Code block styling */
.code-block {
font-family: var(--font-mono);
font-size: 0.875rem;
line-height: 1.6;
}
.code-block .function { color: var(--color-text-primary); }
.code-block .type { color: var(--color-text-secondary); }
.code-block .comment { color: #8a868080; }
.code-block .string { color: #e86101b3; }
.code-block .number { color: #e86101cc; }
.code-block .keyword { color: var(--color-amber); }
/* Chip component */
.chip {
display: inline-block;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.05em;
background-color: var(--color-amber-10);
border: 1px solid var(--color-amber-20);
color: var(--color-amber);
}
/* Section label */
.section-label {
font-family: var(--font-mono);
font-size: 0.875rem;
color: var(--color-amber);
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* Text link with underline animation */
.text-link {
position: relative;
color: var(--color-amber);
text-decoration: none;
}
.text-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 1px;
background-color: var(--color-amber);
transition: width 0.3s ease;
}
.text-link:hover::after {
width: 100%;
}

69
src/app/hobbies/page.tsx Normal file
View file

@ -0,0 +1,69 @@
import { hobbies, hobbiesPage } from "@/data/hobbies";
import { Icon } from "@/lib/icons";
export default function HobbiesPage() {
return (
<main className="min-h-screen bg-deep-space">
{/* Header */}
<section className="pt-32 pb-20 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto space-y-6">
<p className="section-label">{hobbiesPage.label}</p>
<h1
className="font-display font-bold text-text-primary leading-[0.9] tracking-tight"
style={{ fontSize: "clamp(2.5rem, 8vw, 5rem)" }}
>
{hobbiesPage.pageTitle}
</h1>
<p className="text-text-secondary max-w-2xl text-lg leading-relaxed">
{hobbiesPage.pageSubtitle}
</p>
</div>
</section>
{/* Hobbies Content */}
<section className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto space-y-24">
{hobbies.map((hobby, index) => {
const isEven = index % 2 === 0;
return (
<div
key={hobby.id}
className={`grid grid-cols-1 lg:grid-cols-[1fr_2fr] gap-12 items-start ${
isEven ? "" : "lg:grid-cols-[2fr_1fr]"
}`}
>
<div className={`space-y-6 ${isEven ? "" : "lg:order-2"}`}>
<div className="w-12 h-12 flex items-center justify-center border border-warm-grey">
<Icon name={hobby.icon} size={24} className="text-amber" />
</div>
<h2 className="font-display font-semibold text-text-primary text-3xl tracking-tight">
{hobby.title}
</h2>
</div>
<div className={`space-y-6 ${isEven ? "" : "lg:order-1"}`}>
{hobby.description.map((paragraph, i) => (
<p key={i} className="text-text-secondary leading-relaxed">
{paragraph}
</p>
))}
</div>
</div>
);
})}
</div>
</section>
{/* Quote */}
<section className="py-20 px-[5vw] bg-charcoal">
<div className="max-w-[800px] mx-auto text-center space-y-6">
<p className="font-display text-text-primary text-2xl md:text-3xl italic leading-relaxed">
&ldquo;{hobbiesPage.quote}&rdquo;
</p>
<p className="text-text-secondary text-sm font-mono">
{hobbiesPage.quoteAuthor}
</p>
</div>
</section>
</main>
);
}

56
src/app/layout.tsx Normal file
View file

@ -0,0 +1,56 @@
import type { Metadata } from "next";
import { Space_Grotesk, Inter, IBM_Plex_Mono } from "next/font/google";
import "./globals.css";
import Navigation from "@/components/Navigation";
import Footer from "@/components/Footer";
import PreviewBanner from "@/components/PreviewBanner";
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-space-grotesk",
display: "swap",
});
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "600"],
variable: "--font-inter",
display: "swap",
});
const ibmPlexMono = IBM_Plex_Mono({
subsets: ["latin"],
weight: ["400", "500"],
variable: "--font-ibm-plex-mono",
display: "swap",
});
export const metadata: Metadata = {
title: "Mike Karl — Portfolio",
description:
"Mike Karl — Systementwickler & Creative Technologist aus Köln. Portfolio mit Projekten aus Systems Programming, Computer Graphics und Low-Level-Entwicklung.",
icons: {
icon: "/favicon.svg",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="de"
className={`${spaceGrotesk.variable} ${inter.variable} ${ibmPlexMono.variable}`}
>
<body className="min-h-screen bg-deep-space text-text-primary antialiased">
<PreviewBanner />
<Navigation />
{children}
<Footer />
</body>
</html>
);
}

19
src/app/page.tsx Normal file
View file

@ -0,0 +1,19 @@
import HeroSection from "@/sections/HeroSection";
import AboutSection from "@/sections/AboutSection";
import HobbiesSection from "@/sections/HobbiesSection";
import ProjectsSection from "@/sections/ProjectsSection";
import TimelineSection from "@/sections/TimelineSection";
import ContactSection from "@/sections/ContactSection";
export default function HomePage() {
return (
<main>
<HeroSection />
<AboutSection />
<HobbiesSection />
<ProjectsSection />
<TimelineSection />
<ContactSection />
</main>
);
}

View file

@ -0,0 +1,23 @@
import ProjectPageLayout from "@/components/ProjectPageLayout";
import { emulatorProject } from "@/data/projects";
export default function ProjectEmulatorPage() {
return (
<ProjectPageLayout
title={emulatorProject.title}
subtitle={emulatorProject.subtitle}
tags={emulatorProject.tags}
year={emulatorProject.year}
duration={emulatorProject.duration}
status={emulatorProject.status}
description={emulatorProject.description}
image={emulatorProject.image}
imageAlt={emulatorProject.imageAlt}
features={emulatorProject.features}
stats={emulatorProject.stats}
gallery={emulatorProject.gallery}
prevProject={emulatorProject.prevProject}
nextProject={emulatorProject.nextProject}
/>
);
}

View file

@ -0,0 +1,23 @@
import ProjectPageLayout from "@/components/ProjectPageLayout";
import { raycasterProject } from "@/data/projects";
export default function ProjectRaycasterPage() {
return (
<ProjectPageLayout
title={raycasterProject.title}
subtitle={raycasterProject.subtitle}
tags={raycasterProject.tags}
year={raycasterProject.year}
duration={raycasterProject.duration}
status={raycasterProject.status}
description={raycasterProject.description}
image={raycasterProject.image}
imageAlt={raycasterProject.imageAlt}
features={raycasterProject.features}
stats={raycasterProject.stats}
gallery={raycasterProject.gallery}
prevProject={raycasterProject.prevProject}
nextProject={raycasterProject.nextProject}
/>
);
}

View file

@ -0,0 +1,23 @@
import ProjectPageLayout from "@/components/ProjectPageLayout";
import { raytracerProject } from "@/data/projects";
export default function ProjectRaytracerPage() {
return (
<ProjectPageLayout
title={raytracerProject.title}
subtitle={raytracerProject.subtitle}
tags={raytracerProject.tags}
year={raytracerProject.year}
duration={raytracerProject.duration}
status={raytracerProject.status}
description={raytracerProject.description}
image={raytracerProject.image}
imageAlt={raytracerProject.imageAlt}
features={raytracerProject.features}
stats={raytracerProject.stats}
gallery={raytracerProject.gallery}
prevProject={raytracerProject.prevProject}
nextProject={raytracerProject.nextProject}
/>
);
}

74
src/app/projects/page.tsx Normal file
View file

@ -0,0 +1,74 @@
import Link from "next/link";
import { projects, projectsPage } from "@/data/projects";
import { ArrowRight } from "lucide-react";
export default function ProjectsPage() {
return (
<main className="min-h-screen bg-deep-space">
{/* Header */}
<section className="pt-32 pb-20 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto space-y-6">
<p className="section-label">{projectsPage.pageLabel}</p>
<h1
className="font-display font-bold text-text-primary leading-[0.9] tracking-tight"
style={{ fontSize: "clamp(2.5rem, 8vw, 5rem)" }}
>
{projectsPage.pageTitle}
</h1>
<p className="text-text-secondary max-w-2xl text-lg leading-relaxed">
{projectsPage.subtitle}
</p>
</div>
</section>
{/* Project Grid */}
<section className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto space-y-16">
{projects.map((project) => (
<div
key={project.id}
className="grid grid-cols-1 lg:grid-cols-[1fr_2fr] gap-12 items-start border-t border-warm-grey pt-12"
>
<div className="border border-warm-grey overflow-hidden group/image">
<img
src={project.image}
alt={project.title}
className="w-full h-auto group-hover/image:scale-105 transition-transform duration-700 ease-out"
loading="lazy"
/>
</div>
<div className="space-y-6">
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span key={tag} className="chip">
{tag}
</span>
))}
</div>
<h2 className="font-display font-semibold text-text-primary text-3xl tracking-tight">
{project.title}
</h2>
<p className="text-text-secondary leading-relaxed">
{project.description}
</p>
<p className="text-text-secondary leading-relaxed">
{projectsPage.detailText}
</p>
<Link
href={project.link}
className="inline-flex items-center gap-2 text-amber hover:text-amber-hover transition-colors group"
>
<span className="text-link">{projectsPage.linkText}</span>
<ArrowRight
size={18}
className="group-hover:translate-x-1 transition-transform"
/>
</Link>
</div>
</div>
))}
</div>
</section>
</main>
);
}

5
src/app/template.tsx Normal file
View file

@ -0,0 +1,5 @@
import PageTransition from "@/components/PageTransition";
export default function Template({ children }: { children: React.ReactNode }) {
return <PageTransition>{children}</PageTransition>;
}

55
src/app/timeline/page.tsx Normal file
View file

@ -0,0 +1,55 @@
import { timeline, timelinePage } from "@/data/timeline";
export default function TimelinePage() {
return (
<main className="min-h-screen bg-deep-space">
{/* Header */}
<section className="pt-32 pb-20 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto space-y-6">
<p className="section-label">{timelinePage.label}</p>
<h1
className="font-display font-bold text-text-primary leading-[0.9] tracking-tight"
style={{ fontSize: "clamp(2.5rem, 8vw, 5rem)" }}
>
{timelinePage.pageTitle}
</h1>
<p className="text-text-secondary max-w-2xl text-lg leading-relaxed">
{timelinePage.subtitle}
</p>
</div>
</section>
{/* Timeline Content */}
<section className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto space-y-16">
{timeline.map((item) => (
<div
key={item.year}
className="grid grid-cols-1 md:grid-cols-[120px_1fr] gap-8 border-t border-warm-grey pt-12"
>
<span className="font-mono text-amber text-xl">{item.year}</span>
<div className="space-y-4">
<h2 className="font-display font-semibold text-text-primary text-2xl tracking-tight">
{item.title}
</h2>
<p className="text-text-secondary leading-relaxed">
{item.description}
</p>
<p className="text-text-secondary leading-relaxed">
{timelinePage.detailText}
</p>
<div className="flex flex-wrap gap-2 pt-2">
{item.tags.map((tag) => (
<span key={tag} className="chip">
{tag}
</span>
))}
</div>
</div>
</div>
))}
</div>
</section>
</main>
);
}

16
src/components/Footer.tsx Normal file
View file

@ -0,0 +1,16 @@
import { site, footerContent } from "../data/site";
export default function Footer() {
return (
<footer className="border-t border-warm-grey py-8 px-[5vw]">
<div className="max-w-[1200px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-sm text-text-secondary">
{footerContent.copyright(new Date().getFullYear(), site.name)}
</p>
<p className="text-sm text-text-secondary font-mono">
{footerContent.builtWith}
</p>
</div>
</footer>
);
}

View file

@ -0,0 +1,124 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Menu, X } from "lucide-react";
import { site, navLinks, socialLinks } from "../data/site";
import { Icon } from "@/lib/icons";
export default function Navigation() {
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const pathname = usePathname();
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 50);
window.addEventListener("scroll", handleScroll, { passive: true });
handleScroll();
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
setMobileOpen(false);
window.scrollTo(0, 0);
}, [pathname]);
return (
<header
className={`fixed top-0 left-0 right-0 z-50 transition-colors duration-300 [will-change:transform] ${
scrolled
? "bg-deep-space/95 border-b border-warm-grey/50"
: "bg-transparent border-b border-transparent"
}`}
>
<nav className="max-w-[1400px] mx-auto px-[5vw] py-5 flex items-center justify-between">
<Link
href="/"
className="font-display font-bold text-text-primary tracking-tight text-lg"
>
{site.name.toUpperCase()}
</Link>
{/* Desktop Nav */}
<div className="hidden md:flex items-center gap-8">
<ul className="flex items-center gap-6">
{navLinks.map((link) => {
const isActive = pathname === link.href;
return (
<li key={link.href}>
<Link
href={link.href}
className={`text-sm transition-colors ${
isActive
? "text-text-primary"
: "text-text-secondary hover:text-text-primary"
}`}
>
{link.label}
</Link>
</li>
);
})}
</ul>
<div className="flex items-center gap-4 border-l border-warm-grey pl-6">
{socialLinks.map((link) => (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
aria-label={link.label}
className="text-text-secondary hover:text-amber transition-colors"
>
<Icon name={link.icon} size={18} />
</a>
))}
</div>
</div>
{/* Mobile Menu Button */}
<button
className="md:hidden text-text-primary"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label="Toggle menu"
>
{mobileOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</nav>
{/* Mobile Menu */}
{mobileOpen && (
<div className="md:hidden bg-deep-space/95 border-t border-warm-grey/50 px-[5vw] py-8">
<ul className="space-y-4 mb-8">
{navLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="block text-lg text-text-primary"
onClick={() => setMobileOpen(false)}
>
{link.label}
</Link>
</li>
))}
</ul>
<div className="flex items-center gap-6">
{socialLinks.map((link) => (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
aria-label={link.label}
className="text-text-secondary hover:text-amber transition-colors"
>
<Icon name={link.icon} size={20} />
</a>
))}
</div>
</div>
)}
</header>
);
}

View file

@ -0,0 +1,38 @@
"use client";
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
export default function PageTransition({
children,
}: {
children: React.ReactNode;
}) {
const containerRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const el = containerRef.current;
if (!el) return;
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (prefersReducedMotion) {
gsap.set(el, { opacity: 1, y: 0 });
return;
}
const ctx = gsap.context(() => {
gsap.fromTo(
el,
{ opacity: 0, y: 16 },
{ opacity: 1, y: 0, duration: 0.55, ease: "power3.out", delay: 0.05 }
);
});
return () => ctx.revert();
}, []);
return <div ref={containerRef}>{children}</div>;
}

View file

@ -0,0 +1,123 @@
"use client";
import { useEffect, useRef } from "react";
interface Particle {
x: number;
y: number;
baseX: number;
baseY: number;
size: number;
}
export default function ParticleGrid() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animRef = useRef<number>(0);
const mouseRef = useRef({ x: -1000, y: -1000 });
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let particles: Particle[] = [];
let time = 0;
const init = () => {
const dpr = window.devicePixelRatio || 1;
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.scale(dpr, dpr);
const spacing = Math.min(w, h) / 35;
const cols = Math.ceil(w / spacing) + 1;
const rows = Math.ceil(h / spacing) + 1;
particles = [];
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
particles.push({
x: i * spacing,
y: j * spacing,
baseX: i * spacing,
baseY: j * spacing,
size: 2 + Math.random() * 1.5,
});
}
}
};
const animate = () => {
time += 0.015;
const w = window.innerWidth;
const h = window.innerHeight;
ctx.clearRect(0, 0, w, h);
const mouse = mouseRef.current;
const radius = 180;
for (const p of particles) {
const waveX = Math.sin(p.baseY * 0.008 + time) * 8;
const waveY = Math.cos(p.baseX * 0.008 + time) * 6;
let px = p.baseX + waveX;
let py = p.baseY + waveY;
const dx = px - mouse.x;
const dy = py - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < radius) {
const force = (radius - dist) / radius;
const angle = Math.atan2(dy, dx);
px += Math.cos(angle) * force * 30;
py += Math.sin(angle) * force * 30;
}
const finalDist = Math.sqrt((px - mouse.x) ** 2 + (py - mouse.y) ** 2);
const alpha = finalDist < radius ? 0.3 + (1 - finalDist / radius) * 0.5 : 0.25;
const sizeMult = finalDist < radius ? 1.5 : 1;
ctx.beginPath();
ctx.arc(px, py, p.size * sizeMult, 0, Math.PI * 2);
ctx.fillStyle = `rgba(232, 97, 1, ${alpha})`;
ctx.fill();
}
animRef.current = requestAnimationFrame(animate);
};
const handleMouseMove = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
mouseRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
};
const handleMouseLeave = () => {
mouseRef.current = { x: -1000, y: -1000 };
};
init();
animate();
window.addEventListener("resize", init);
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("mouseleave", handleMouseLeave);
return () => {
cancelAnimationFrame(animRef.current);
window.removeEventListener("resize", init);
canvas.removeEventListener("mousemove", handleMouseMove);
canvas.removeEventListener("mouseleave", handleMouseLeave);
};
}, []);
return (
<canvas
ref={canvasRef}
aria-hidden="true"
className="absolute inset-0 w-full h-full"
style={{ display: "block" }}
/>
);
}

View file

@ -0,0 +1,9 @@
export default function PreviewBanner() {
return (
<div className="fixed top-0 left-0 z-[60] pointer-events-none">
<div className="absolute w-[320px] bg-amber text-deep-space text-center text-sm font-bold font-mono uppercase tracking-widest py-2.5 rotate-[-45deg] top-10 -left-20 shadow-lg">
Preview Build
</div>
</div>
);
}

View file

@ -0,0 +1,229 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { useScrollReveal } from "../hooks/useScrollReveal";
import { projectLayout } from "../data/projects";
interface GalleryItem {
src: string;
alt: string;
}
interface Stat {
label: string;
value: string;
}
interface Feature {
title: string;
description: string;
}
interface AdjacentProject {
title: string;
link: string;
}
interface ProjectPageLayoutProps {
title: string;
subtitle: string;
tags: string[];
year: string;
duration: string;
status: string;
description: string[];
image: string;
imageAlt: string;
features: Feature[];
stats: Stat[];
gallery: GalleryItem[];
prevProject: AdjacentProject;
nextProject: AdjacentProject;
}
export default function ProjectPageLayout({
title,
subtitle,
tags,
year,
duration,
status,
description,
image,
imageAlt,
features,
stats,
gallery,
prevProject,
nextProject,
}: ProjectPageLayoutProps) {
const router = useRouter();
const heroRef = useScrollReveal<HTMLDivElement>({ stagger: 0.1, start: "top 90%" });
const descRef = useScrollReveal<HTMLDivElement>({ stagger: 0.15 });
const techRef = useScrollReveal<HTMLDivElement>({ stagger: 0.15 });
const galleryRef = useScrollReveal<HTMLDivElement>({ stagger: 0.1 });
return (
<main className="min-h-screen bg-deep-space">
{/* Hero */}
<section className="pt-32 pb-20 px-[5vw] bg-charcoal">
<div ref={heroRef} className="max-w-[1200px] mx-auto space-y-8">
<button
onClick={() => router.back()}
className="inline-flex items-center gap-2 text-link text-amber hover:opacity-80 transition-opacity cursor-pointer"
>
<ArrowLeft size={16} />
<span>{projectLayout.backLabel}</span>
</button>
<div className="space-y-4">
<h1
className="font-display font-bold text-text-primary leading-[0.9] tracking-tight"
style={{ fontSize: "clamp(2.5rem, 8vw, 6rem)" }}
>
{title}
</h1>
<p className="font-display font-medium text-amber text-xl md:text-2xl">
{subtitle}
</p>
</div>
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<span key={tag} className="chip">
{tag}
</span>
))}
</div>
<div className="font-mono text-text-secondary text-sm flex flex-wrap gap-x-6 gap-y-2">
<span>{projectLayout.yearLabel}: {year}</span>
<span>|</span>
<span>{projectLayout.durationLabel}: {duration}</span>
<span>|</span>
<span>{projectLayout.statusLabel}: {status}</span>
</div>
</div>
</section>
{/* Project Description */}
<section className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto grid grid-cols-1 lg:grid-cols-[3fr_2fr] gap-16 items-start">
<div ref={descRef} className="space-y-8">
<h2 className="font-display font-semibold text-text-primary text-3xl md:text-4xl tracking-tight">
{projectLayout.descriptionTitle}
</h2>
<div className="space-y-6 text-text-secondary leading-relaxed">
{description.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
</div>
<div className="border border-warm-grey overflow-hidden">
<img
src={image}
alt={imageAlt}
className="w-full h-auto"
loading="lazy"
/>
</div>
</div>
</section>
{/* Technical Implementation */}
<section className="py-20 md:py-32 px-[5vw] bg-charcoal">
<div ref={techRef} className="max-w-[800px] mx-auto space-y-12">
<h2 className="font-display font-semibold text-text-primary text-3xl md:text-4xl tracking-tight">
{projectLayout.techTitle}
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature) => (
<div
key={feature.title}
className="border border-warm-grey p-6 space-y-4"
>
<h3 className="font-display font-medium text-text-primary text-lg">
{feature.title}
</h3>
<p className="text-text-secondary text-sm leading-relaxed">
{feature.description}
</p>
</div>
))}
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 pt-8 border-t border-warm-grey">
{stats.map((stat) => (
<div key={stat.label} className="text-center space-y-2">
<p className="font-display font-bold text-text-primary text-2xl md:text-3xl text-amber">
{stat.value}
</p>
<p className="text-text-secondary text-xs font-mono uppercase tracking-wider">
{stat.label}
</p>
</div>
))}
</div>
</div>
</section>
{/* Gallery */}
<section className="py-20 md:py-32 px-[5vw]">
<div ref={galleryRef} className="max-w-[1200px] mx-auto space-y-12">
<h2 className="font-display font-semibold text-text-primary text-3xl md:text-4xl tracking-tight">
{projectLayout.galleryTitle}
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{gallery.map((item) => (
<div
key={item.src}
className="border border-warm-grey overflow-hidden"
>
<img
src={item.src}
alt={item.alt}
className="w-full h-48 object-cover hover:scale-105 transition-transform duration-500"
loading="lazy"
/>
</div>
))}
</div>
</div>
</section>
{/* Prev / Next Navigation */}
<section className="py-20 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto flex flex-col md:flex-row items-center justify-between gap-6">
<Link
href={prevProject.link}
replace
className="inline-flex items-center gap-3 px-6 py-3 border border-warm-grey text-text-primary hover:border-amber hover:text-amber transition-colors"
>
<ArrowLeft size={16} />
<div className="text-left">
<p className="text-text-secondary text-xs mb-0.5">{projectLayout.prevProjectLabel}</p>
<p className="font-display font-medium text-sm">{prevProject.title}</p>
</div>
</Link>
<Link
href={nextProject.link}
replace
className="inline-flex items-center gap-3 px-6 py-3 border border-warm-grey text-text-primary hover:border-amber hover:text-amber transition-colors"
>
<div className="text-right">
<p className="text-text-secondary text-xs mb-0.5">{projectLayout.nextProjectLabel}</p>
<p className="font-display font-medium text-sm">{nextProject.title}</p>
</div>
<ArrowRight size={16} />
</Link>
</div>
</section>
</main>
);
}

59
src/data/about.ts Normal file
View file

@ -0,0 +1,59 @@
export const values = [
{
icon: "Crosshair",
title: "Precision",
text: "Jeder Taktzyklus zählt. Jeder Speicherzugriff ist bewusst. Ich baue Systeme, die ihre Ressourcen kennen und respektieren.",
quote: "Effizienz ist nicht Optimierung – es ist gute Architektur.",
},
{
icon: "Search",
title: "Curiosity",
text: "Vom 6502-Chip bis zum Raytracing-Algorithmus – ich gehe tiefer, bis ich die Ebene verstehe, auf der alles zusammenkommt.",
quote: "Verstehen beginnt dort, wo die Abstraktion endet.",
},
{
icon: "Timer",
title: "Patience",
text: "Gute Software ist wie Astrofotografie: Stunden der Vorbereitung für den einen Moment, in dem alles zusammenkommt.",
quote: "Das Beste entsteht in der Zeit, die man ihm gibt.",
},
];
export const aboutContent = {
paragraphs: [
"Ich baue Systeme – von der Transistor-Ebene bis zur Benutzeroberfläche. Meine Arbeit beginnt dort, wo abstrakte Konzepte auf konkrete Hardware treffen. Ein 6502-Emulator, der jeden Taktzyklus simuliert. Ein Raytracer, der Lichtstrahl für Lichtstrahl berechnet. Ich interessiere mich für die Dinge, die man nicht sieht: Speicherlayout, Cache-Hierarchien, die stille Effizienz guter Architektur.",
"Neben dem Code fotografiere ich den Nachthimmel – lange Belichtungszeiten, Tracking-Montierungen, das Sammeln von Licht über Stunden. Das Prinzip ist ähnlich: Geduld, Präzision, das Vertrauen auf einen Prozess, dessen Ergebnis man erst am Ende sieht.",
"Ich arbeite am liebsten an Projekten, die verbinden: Low-Level-Performance mit menschlicher Erfahrung, technische Tiefe mit visueller Klarheit. Wenn das klingt, als könnten wir etwas bauen – schreib mir.",
],
};
export const aboutPage = {
label: "01 / About",
pageTitle: "Über mich",
pageSubtitle:
"Systementwickler & Creative Technologist aus Köln. Ich baue Dinge von der Transistor-Ebene bis zur Benutzeroberfläche.",
linkText: "Mehr über meine Hobbies",
sidebarFocusTitle: "Schwerpunkte",
sidebarFocusItems: [
"Systems Programming",
"Computer Graphics",
"Low-Level Optimization",
"Emulation & Simulation",
"Algorithm Design",
],
sidebarLocationTitle: "Standort",
sidebarHobbiesTitle: "Hobbies",
sidebarHobbiesItems: ["Motorradfahren", "Astrofotografie"],
sidebarHobbiesLink: "Details ansehen",
valuesTitle: "Werte",
philosophyTitle: "Philosophie",
philosophyText: [
"Hier wird bald eine ausführliche Darstellung meiner Entwicklungsphilosophie stehen – wie ich Systeme angehe, warum ich tiefer gehe als nötig, und welche Prinzipien meine Arbeit leiten.",
"Derzeit ist dies ein Platzhalter für zukünftigen Content, der meinen Ansatz zu Software-Architektur, Performance-Bewusstsein und handwerklicher Qualität detailliert beschreibt.",
],
backgroundTitle: "Hintergrund",
backgroundText: [
"Ein weiterer Abschnitt, der meinen Werdegang, meine Motivation und die Schlüsselmomente meiner Entwicklung als Systementwickler beleuchtet.",
"Von ersten Schritten in C und C++ über die Faszination für Computergrafik bis hin zur strukturierten Ausbildung – diese Geschichte wird hier bald vollständig erzählt.",
],
};

20
src/data/contact.ts Normal file
View file

@ -0,0 +1,20 @@
export const contactContent = {
heading: "Lass uns bauen.",
text: "Ich arbeite gerne an Projekten, die verbinden – Low-Level-Performance mit menschlicher Erfahrung. Wenn du etwas baust, das präzise, schnell oder einfach schön sein soll, schreib mir.",
cta: "Kontakt aufnehmen",
};
export const contactPage = {
label: "05 / Kontakt",
pageSubtitle:
"Ich arbeite gerne an Projekten, die verbinden – Low-Level-Performance mit menschlicher Erfahrung.",
contentTitle: "Zusammenarbeit",
contentPlaceholder1:
"Dieser Bereich wird bald um konkrete Informationen zu meinen Arbeitsweisen, bevorzugten Technologien und dem besten Weg zur Kontaktaufnahme erweitert.",
contentPlaceholder2:
"Ob du ein Projekt planst, das präzise, schnell oder einfach schön sein soll – hier wird bald stehen, wie wir am besten zusammenfinden.",
sidebarEmailTitle: "Email",
sidebarSocialTitle: "Social",
sidebarSocialGithub: "GitHub",
sidebarSocialLinkedin: "LinkedIn",
};

38
src/data/hobbies.ts Normal file
View file

@ -0,0 +1,38 @@
export const hobbiesPage = {
label: "02 / Hobbies",
sectionSubtitle: "Was ich außerhalb des Terminals mache.",
pageTitle: "Außerhalb des Terminals",
pageSubtitle:
"Motorradfahren und Astrofotografie – zwei Welten, die Geduld, Präzision und das Vertrauen auf einen Prozess verlangen.",
linkText: "Mehr erfahren",
quote:
"Gute Software ist wie Astrofotografie: Stunden der Vorbereitung für den einen Moment, in dem alles zusammenkommt.",
quoteAuthor: "— Eines der Prinzipien, die Code und Hobby verbinden",
};
export const hobbies = [
{
id: "motorcycle",
icon: "Bike",
title: "Motorradfahren",
shortDescription:
"Freiheit auf zwei Rädern. Die Straße als Ausgleich zum Code.",
description: [
"Motorradfahren ist für mich der perfekte Gegenpol zur Arbeit am Rechner. Während Code abstrakt und gedanklich ist, ist Motorradfahren physisch und unmittelbar – Geschwindigkeit, Kurven, die Reaktion des Fahrwerks auf den Asphalt.",
"Die Präzision, die beim Fahren auf der Landstraße gefragt ist, erinnert mich an Low-Level-Programmierung: jedes Input muss bewusst sein, jede Reaktion antizipiert. Es trainiert ein Gespür für Systemverhalten unter Stress.",
"Hier wird bald mehr über Touren, die Maschine und die Philosophie hinter dem Fahren stehen.",
],
},
{
id: "astrophotography",
icon: "Camera",
title: "Astrofotografie",
shortDescription:
"Licht sammeln über Stunden. Geduld, Präzision, der Moment.",
description: [
"Astrofotografie ist das Sammeln von Licht über Stunden – lange Belichtungszeiten, Tracking-Montierungen, das Warten auf den perfekten Moment, in dem alles zusammenkommt.",
"Das Prinzip ist ähnlich wie bei guter Software: Stunden der Vorbereitung für den einen Moment, in dem alles funktioniert. Geduld, Präzision, das Vertrauen auf einen Prozess, dessen Ergebnis man erst am Ende sieht.",
"Hier wird bald mehr über Equipment, Aufnahmetechniken und die besten Orte für den Nachthimmel stehen.",
],
},
];

207
src/data/projects.ts Normal file
View file

@ -0,0 +1,207 @@
export const projects = [
{
id: "emulator",
title: "6502 Emulator",
description:
"Cycle-accurate CPU-Simulation in C++ mit eigenem Assembler-Parser.",
image: "/assets/emulator-screenshot.jpg",
tags: ["C++", "6502 Assembly", "Systems Programming"],
link: "/project/emulator",
},
{
id: "raytracer",
title: "Software Raytracer",
description:
"Monte-Carlo-Pfad-Tracing-Renderer in C. Multithreading, BVH-Beschleunigung, sanfte Schatten.",
image: "/assets/raytracer-render.jpg",
tags: ["C", "Computer Graphics", "Multithreading"],
link: "/project/raytracer",
},
{
id: "raycaster",
title: "Raycasting Engine",
description:
"Wolfenstein-3D-Style-Renderer mit DDA-Algorithmus und Textur-Mapping.",
image: "/assets/raycaster-screenshot.jpg",
tags: ["C++", "Game Dev", "Algorithms"],
link: "/project/raycaster",
},
];
export const emulatorProject = {
title: "6502 Emulator",
subtitle: "Cycle-accurate CPU-Simulation in C++",
tags: ["C++", "6502 Assembly", "Systems Programming", "Emulation"],
year: "2023",
duration: "6 Monate",
status: "Abgeschlossen",
description: [
"Der MOS 6502 ist der Chip, der die Home-Computer-Revolution antrieb: Apple II, Commodore 64, Nintendo Entertainment System. Er hat 3510 Transistoren. Jeder einzelne zählt.",
"Mein Emulator simuliert jeden Taktzyklus des 6502 mit Cycle-Accuracy – das bedeutet, dass die Timing-Korrektheit bis auf einen Takt genau stimmt. Das ist entscheidend für Systeme, die auf präzise Hardware-Timing angewiesen sind, wie der C64 mit seinem VIC-II-Grafikchip.",
"Der Emulator enthält einen eigenen Assembler-Parser, der 6502-Assembly-Syntax in Maschinencode übersetzt. Keine externen Abhängigkeiten – nur C++, Standardbibliotheken und das Verständnis der Architektur.",
],
image: "/assets/emulator-screenshot.jpg",
imageAlt: "6502 Emulator Terminal",
features: [
{
title: "Cycle-Accurate Simulation",
description:
"Jeder Befehl wird mit korrektem Takt-Verbrauch simuliert. Seiten-Effekte, Page-Crossing-Penalties, genaue Adressierungsmodi.",
},
{
title: "Assembler-Parser",
description:
"Eigener zweipassiger Assembler mit Label-Auflösung, Expression-Evaluation und Fehlerberichterstattung.",
},
{
title: "Speichersystem",
description:
"64KB Adressraum mit korrekter Memory-Mapping-Logik. RAM, ROM und Memory-Mapped I/O.",
},
],
stats: [
{ label: "Transistoren simuliert", value: "3,510" },
{ label: "Implementierte Befehle", value: "56" },
{ label: "Adressierungsmodi", value: "13" },
{ label: "Taktzyklen getestet", value: "1M+" },
],
gallery: [
{ src: "/assets/emulator-screenshot.jpg", alt: "6502 Emulator Terminal" },
{ src: "/assets/emulator-display.jpg", alt: "Assembler Code" },
{ src: "/assets/debugger-view.jpg", alt: "Debugger Register State" },
],
prevProject: {
title: "Raycasting Engine",
link: "/project/raycaster",
},
nextProject: {
title: "Software Raytracer",
link: "/project/raytracer",
},
};
export const raytracerProject = {
title: "Software Raytracer",
subtitle: "Monte-Carlo-Pfad-Tracing in C",
tags: ["C", "Computer Graphics", "Multithreading"],
year: "2024",
duration: "8 Monate",
status: "Abgeschlossen",
description: [
"Dieser Raytracer berechnet Licht. Nicht approximiert, nicht geschätzt – simuliert. Jeder Pixel ist das Ergebnis von Millionen Lichtstrahlen, die durch eine Szene bouncen, sich brechen und absorbieren.",
"Geschrieben in C von Grund auf. Keine Grafik-Bibliotheken, keine Hardware-Beschleunigung. Nur Mathematik, Speicher und so viele CPU-Kerne, wie das System hergibt.",
"Das Projekt war eine Reise in die Computergrafik: von Vektor-Mathematik über Bounding Volume Hierarchies bis hin zur Farbwissenschaft von ACES-Tonemapping.",
],
image: "/assets/raytracer-render.jpg",
imageAlt: "Raytracer Render Output",
features: [
{
title: "Monte-Carlo-Pfad-Tracing",
description:
"Rekursive Lichtstrahl-Simulation mit Russian Roulette-Terminierung. Diffuse, spiegelnde und transparente Materialien.",
},
{
title: "BVH-Beschleunigung",
description:
"Bounding Volume Hierarchy mit SAH-Heuristik für schnelle Ray-Schnitt-Tests. Rendert komplexe Szenen in Sekunden statt Stunden.",
},
{
title: "Multithreading",
description:
"Thread-Pool mit Work-Stealing-Scheduling. Nahezu lineare Skalierung mit Kernanzahl.",
},
],
stats: [
{ label: "Strahlen pro Pixel", value: "1M+" },
{ label: "CPU-Kerne genutzt", value: "8" },
{ label: "Externe Bibliotheken", value: "0" },
{ label: "Rendern komplexer Szene", value: "<2s" },
],
gallery: [
{ src: "/assets/raytracer-render.jpg", alt: "Raytracer Render Output" },
{ src: "/assets/raytracer-cornell.jpg", alt: "Cornell Box Render" },
{ src: "/assets/raytracer-metal.jpg", alt: "Metallic Scene Render" },
],
prevProject: {
title: "6502 Emulator",
link: "/project/emulator",
},
nextProject: {
title: "Raycasting Engine",
link: "/project/raycaster",
},
};
export const projectLayout = {
backLabel: "Zurück",
yearLabel: "Jahr",
durationLabel: "Dauer",
statusLabel: "Status",
descriptionTitle: "Das Projekt",
techTitle: "Technische Umsetzung",
galleryTitle: "Galerie",
prevProjectLabel: "Vorheriges Projekt",
nextProjectLabel: "Nächstes Projekt",
};
export const projectsPage = {
sectionLabel: "03 / Ausgewählte Projekte",
pageLabel: "03 / Projekte",
pageTitle: "Ausgewählte Projekte",
subtitle:
"Systeme, die ich aus der Transistor-Ebene heraus gebaut habe.",
caseStudyLabel: "Case Study",
detailText:
"Dieser Abschnitt wird bald eine detailliertere Beschreibung des Projekts enthalten – Motivation, Herausforderungen, Lernerfahrungen und technische Tiefenblicke, die über die Kurzbeschreibung auf der Startseite hinausgehen.",
linkText: "Zur Case Study",
};
export const raycasterProject = {
title: "Raycasting Engine",
subtitle: "Wolfenstein-3D-Style-Renderer mit DDA-Algorithmus",
tags: ["C++", "Game Dev", "Algorithms"],
year: "2021",
duration: "3 Monate",
status: "Abgeschlossen",
description: [
"Eine Einführung in Echtzeit-Computergrafik: ein Wolfenstein-3D-Style-Raycaster, der mit dem DDA-Algorithmus Wände findet und mit Texturen füllt.",
"Geschrieben in C++ ohne externe Grafik-Engines. Alles von der Bildschirmmaschine bis zum Pixel-Buffer selbst implementiert.",
"Das Projekt lehrte mich die Grundlagen der Rasterisierung, der Performance-Optimierung in Spiel-Loops und die Bedeutung fester Zeitschritte in Echtzeit-Systemen.",
],
image: "/assets/raycaster-screenshot.jpg",
imageAlt: "Raycasting Engine Screenshot",
features: [
{
title: "DDA-Algorithmus",
description:
"Digital Differential Analysis für präzise Wand-Schnitt-Berechnung auf einem 2D-Gitter. Schnell, deterministisch und elegant.",
},
{
title: "Textur-Mapping",
description:
"Korrekte perspektivische Textur-Koordinaten für jede Säule. Keine Verzerrung, keine Nahtstellen.",
},
{
title: "Echtzeit-Performance",
description:
"60 FPS auf moderner Hardware. Das Fundament für ein Verständnis von Frame-Timing und vsync.",
},
],
stats: [
{ label: "FPS Ziel", value: "60" },
{ label: "Texturauflösung", value: "64px" },
{ label: "Sichtweite", value: "∞" },
{ label: "Abhängigkeiten", value: "0" },
],
gallery: [
{ src: "/assets/raycaster-screenshot.jpg", alt: "Raycaster Screenshot" },
],
prevProject: {
title: "Software Raytracer",
link: "/project/raytracer",
},
nextProject: {
title: "6502 Emulator",
link: "/project/emulator",
},
};

33
src/data/site.ts Normal file
View file

@ -0,0 +1,33 @@
export const site = {
name: "Mike Karl",
location: "Grafenau, Germany",
email: "karl.mike2002@gmail.com",
github: "https://github.com/GoranSalzmann",
linkedin: "https://www.linkedin.com/in/mike-karl-a58925381/",
};
export const navLinks = [
{ label: "About", href: "/about" },
{ label: "Hobbies", href: "/hobbies" },
{ label: "Projekte", href: "/projects" },
{ label: "Timeline", href: "/timeline" },
{ label: "Kontakt", href: "/contact" },
];
export const socialLinks = [
{ label: "GitHub", href: site.github, icon: "GithubIcon" },
{ label: "LinkedIn", href: site.linkedin, icon: "LinkedInIcon" },
{ label: "Email", href: `mailto:${site.email}`, icon: "Mail" },
];
export const footerContent = {
copyright: (year: number, name: string) =>
`\u00A9 ${year} ${name}. All rights reserved.`,
builtWith: "Built with Next.js, React & Tailwind CSS",
};
export const heroContent = {
subtitle: "Systementwickler",
nameFirst: "MIKE",
nameLast: "KARL",
};

59
src/data/timeline.ts Normal file
View file

@ -0,0 +1,59 @@
export const timelinePage = {
label: "04 / Werdegang",
subtitle:
'Vom ersten "Hello World" bis zur professionellen Softwareentwicklung.',
pageTitle: "Stationen",
detailText:
"Dieser Abschnitt wird bald um eine detailliertere Reflexion erweitert – welche konkreten Fähigkeiten ich aufgebaut habe, welche Projekte daraus entstanden sind und wie dieser Zeitraum meinen Weg als Entwickler geprägt hat.",
};
export const timeline = [
{
year: "2025",
title: "Ausbildung zum Fachinformatiker",
description:
"Ausbildung zum Fachinformatiker für Anwendungsentwicklung. Der strukturierte Einstieg in professionelle Softwareentwicklung.",
tags: ["Software Engineering", "System Architecture"],
side: "right" as const,
},
{
year: "2024",
title: "Raytracer-Projekt",
description:
"Entwicklung eines Software-Raytracers in C von Grund auf. Monte-Carlo-Pfad-Tracing, BVH-Beschleunigung, Multithreading.",
tags: ["C", "Computer Graphics", "Algorithms"],
side: "left" as const,
},
{
year: "2023",
title: "6502 Emulator",
description:
"Cycle-accurater 6502-Emulator in C++ mit eigenem Assembler-Parser. Das Projekt, das mein Verständnis von CPU-Architektur verändert hat.",
tags: ["C++", "Emulation", "Low-Level"],
side: "right" as const,
},
{
year: "2022",
title: "Erste Berufserfahrung",
description:
"Praktikum und erste Projekte in der Webentwicklung. HTML, CSS, JavaScript – und das Verlangen, tiefer zu gehen.",
tags: ["Web Development", "JavaScript"],
side: "left" as const,
},
{
year: "2021",
title: "Raycasting Engine",
description:
"Wolfenstein-3D-Style-Raycaster mit DDA-Algorithmus und Textur-Mapping. Einführung in Echtzeit-Computergrafik.",
tags: ["C++", "Game Dev", "Graphics"],
side: "right" as const,
},
{
year: "2020",
title: "Beginn der Programmierreise",
description:
"Erste Schritte in C und C++. Das Gefühl, endlich zu verstehen, was unter der Haube passiert.",
tags: ["C", "C++", "Learning"],
side: "left" as const,
},
];

4
src/global.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module '*.css' {
const content: { [className: string]: string };
export default content;
}

View file

@ -0,0 +1,64 @@
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
interface ScrollRevealOptions {
y?: number;
x?: number;
duration?: number;
delay?: number;
stagger?: number;
start?: string;
ease?: string;
}
export function useScrollReveal<T extends HTMLElement>(
options: ScrollRevealOptions = {}
) {
const ref = useRef<T>(null);
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const {
y = 40,
x = 0,
duration = 0.8,
delay = 0,
stagger = 0.1,
start = "top 85%",
ease = "power3.out",
} = options;
const children = el.children.length > 0 ? el.children : [el];
gsap.set(children, { opacity: 0, y, x });
const tween = gsap.to(children, {
opacity: 1,
y: 0,
x: 0,
duration,
delay,
stagger,
ease,
scrollTrigger: {
trigger: el,
start,
toggleActions: "play none none none",
},
});
return () => {
tween.kill();
ScrollTrigger.getAll().forEach((st) => {
if (st.trigger === el) st.kill();
});
};
}, []);
return ref;
}

91
src/lib/icons.tsx Normal file
View file

@ -0,0 +1,91 @@
import {
Crosshair,
Search,
Timer,
Mail,
Bike,
Camera,
} from "lucide-react";
import type { ComponentType } from "react";
function GithubIcon({
size = 18,
className = "",
}: {
size?: number;
className?: string;
}) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
<path d="M9 18c-4.51 2-5-2-7-2" />
</svg>
);
}
function LinkedInIcon({
size = 18,
className = "",
}: {
size?: number;
className?: string;
}) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
<rect width="4" height="12" x="2" y="9" />
<circle cx="4" cy="4" r="2" />
</svg>
);
}
const iconMap: Record<
string,
ComponentType<{ size?: number; className?: string }>
> = {
Crosshair,
Search,
Timer,
Mail,
Bike,
Camera,
GithubIcon,
LinkedInIcon,
};
export function Icon({
name,
size = 18,
className = "",
}: {
name: string;
size?: number;
className?: string;
}) {
const Component = iconMap[name];
if (!Component) {
console.warn(`Icon "${name}" not found`);
return null;
}
return <Component size={size} className={className} />;
}

View file

@ -0,0 +1,45 @@
"use client";
import { useScrollReveal } from "../hooks/useScrollReveal";
import { aboutContent, aboutPage } from "../data/about";
import { ArrowRight } from "lucide-react";
export default function AboutSection() {
const revealRef = useScrollReveal<HTMLDivElement>({ stagger: 0.15 });
const handleProjectsClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
const target = document.querySelector("#projects");
if (target) target.scrollIntoView({ behavior: "smooth" });
};
return (
<section id="about" className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto">
<div ref={revealRef} className="space-y-8">
<p className="section-label">{aboutPage.label}</p>
<div className="max-w-3xl space-y-6 text-text-secondary leading-relaxed">
{aboutContent.paragraphs.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
<div>
<a
href="#projects"
onClick={handleProjectsClick}
className="inline-flex items-center gap-2 text-amber hover:text-amber-hover transition-colors group"
>
<span className="text-link">
{aboutPage.linkText}
</span>
<ArrowRight
size={18}
className="group-hover:translate-x-1 transition-transform"
/>
</a>
</div>
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,38 @@
"use client";
import { useScrollReveal } from "../hooks/useScrollReveal";
import { site } from "../data/site";
import { contactContent, contactPage } from "../data/contact";
import { Mail } from "lucide-react";
export default function ContactSection() {
const revealRef = useScrollReveal<HTMLDivElement>({ stagger: 0.15 });
return (
<section id="contact" className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto">
<div ref={revealRef} className="max-w-2xl space-y-8">
<p className="section-label">{contactPage.label}</p>
<p className="text-text-secondary leading-relaxed text-lg">
{contactContent.text}
</p>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-6">
<a
href={`mailto:${site.email}`}
className="inline-flex items-center gap-3 px-6 py-3 bg-amber text-deep-space font-display font-medium hover:bg-amber-hover transition-colors"
>
<Mail size={18} />
{contactContent.cta}
</a>
<a
href={`mailto:${site.email}`}
className="text-link text-amber font-mono text-sm"
>
{site.email}
</a>
</div>
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,103 @@
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import ParticleGrid from "../components/ParticleGrid";
import { heroContent, site } from "../data/site";
gsap.registerPlugin(ScrollTrigger);
export default function HeroSection() {
const sectionRef = useRef<HTMLElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const bgRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const section = sectionRef.current;
const content = contentRef.current;
const bg = bgRef.current;
if (!section || !content || !bg) return;
const ctx = gsap.context(() => {
gsap.to(bg, {
opacity: 0,
scrollTrigger: {
trigger: section,
start: "top top",
end: "40% top",
scrub: true,
},
});
gsap.to(content, {
opacity: 0,
scale: 0.95,
y: -50,
scrollTrigger: {
trigger: section,
start: "top top",
end: "50% top",
scrub: true,
},
});
}, section);
return () => ctx.revert();
}, []);
return (
<section
ref={sectionRef}
id="hero"
className="relative min-h-[100dvh] flex items-center justify-center overflow-hidden"
>
{/* Particle grid background */}
<div ref={bgRef} className="absolute inset-0 z-0">
<ParticleGrid />
</div>
{/* Amber radial gradient from top */}
<div
className="absolute inset-0 z-[1] pointer-events-none"
style={{
background:
"radial-gradient(ellipse at 50% 0%, rgba(232,97,1,0.08) 0%, transparent 60%)",
}}
/>
{/* Bottom fade to deep space */}
<div
className="absolute inset-0 z-[1] pointer-events-none"
style={{
background:
"linear-gradient(to bottom, transparent 50%, #0F0E0C 100%)",
}}
/>
{/* Content */}
<div
ref={contentRef}
className="relative z-10 text-center px-6"
>
<p className="font-mono text-amber text-sm md:text-base tracking-[0.1em] uppercase mb-6">
{heroContent.subtitle}
</p>
<h1
className="font-display font-bold text-text-primary leading-[0.9] tracking-tight"
style={{ fontSize: "clamp(4rem, 12vw, 10rem)" }}
>
{heroContent.nameFirst}
<br />
{heroContent.nameLast}
</h1>
<div className="flex items-center justify-center gap-4 mt-6">
<div className="w-[60px] h-px bg-amber" />
<p className="font-mono text-text-secondary text-xs md:text-sm tracking-wider">
{site.location}
</p>
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,52 @@
"use client";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { useScrollReveal } from "../hooks/useScrollReveal";
import { hobbies, hobbiesPage } from "../data/hobbies";
import { Icon } from "@/lib/icons";
export default function HobbiesSection() {
const revealRef = useScrollReveal<HTMLDivElement>({ stagger: 0.2 });
return (
<section id="hobbies" className="py-20 md:py-32 px-[5vw]">
<div className="max-w-[1200px] mx-auto">
<div className="space-y-4 mb-12">
<p className="section-label">{hobbiesPage.label}</p>
<p className="text-text-secondary max-w-xl">
{hobbiesPage.sectionSubtitle}
</p>
</div>
<div ref={revealRef} className="grid grid-cols-1 md:grid-cols-2 gap-8">
{hobbies.map((hobby) => (
<div
key={hobby.id}
className="border border-warm-grey p-8 space-y-6 group hover:border-amber/30 transition-colors"
>
<div className="w-10 h-10 flex items-center justify-center border border-warm-grey">
<Icon name={hobby.icon} size={20} className="text-amber" />
</div>
<h3 className="font-display font-semibold text-text-primary text-2xl tracking-tight">
{hobby.title}
</h3>
<p className="text-text-secondary leading-relaxed">
{hobby.shortDescription}
</p>
<Link
href="/hobbies"
className="inline-flex items-center gap-2 text-amber hover:text-amber-hover transition-colors group/link"
>
<span className="text-link">{hobbiesPage.linkText}</span>
<ArrowRight
size={18}
className="group-hover/link:translate-x-1 transition-transform"
/>
</Link>
</div>
))}
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,65 @@
"use client";
import Link from "next/link";
import { useScrollReveal } from "../hooks/useScrollReveal";
import { projects, projectsPage } from "../data/projects";
import { ArrowRight } from "lucide-react";
export default function ProjectsSection() {
const revealRef = useScrollReveal<HTMLDivElement>({ stagger: 0.15 });
return (
<section id="projects" className="py-20 md:py-32 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto">
<div ref={revealRef} className="space-y-12">
<div className="space-y-4">
<p className="section-label">{projectsPage.sectionLabel}</p>
<p className="text-text-secondary max-w-xl">
{projectsPage.subtitle}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project) => (
<Link
key={project.id}
href={project.link}
className="group block border border-warm-grey bg-deep-space overflow-hidden hover:border-amber/50 transition-colors duration-300"
>
<div className="relative overflow-hidden aspect-[4/3]">
<img
src={project.image}
alt={project.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700 ease-out"
loading="lazy"
/>
<div className="absolute inset-0 bg-deep-space/60 translate-y-full group-hover:translate-y-0 transition-transform duration-500 ease-[cubic-bezier(0.33,1,0.68,1)] flex items-center justify-center">
<span className="inline-flex items-center gap-2 text-amber translate-y-3 group-hover:translate-y-0 transition-transform duration-500 ease-[cubic-bezier(0.33,1,0.68,1)] delay-100">
{projectsPage.caseStudyLabel}
<ArrowRight size={16} className="group-hover:translate-x-1 transition-transform duration-300" />
</span>
</div>
</div>
<div className="p-6 space-y-4">
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<span key={tag} className="chip">
{tag}
</span>
))}
</div>
<h3 className="font-display font-semibold text-text-primary text-xl tracking-tight">
{project.title}
</h3>
<p className="text-text-secondary text-sm leading-relaxed">
{project.description}
</p>
</div>
</Link>
))}
</div>
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,73 @@
"use client";
import { useScrollReveal } from "../hooks/useScrollReveal";
import { timeline, timelinePage } from "../data/timeline";
export default function TimelineSection() {
const revealRef = useScrollReveal<HTMLDivElement>({ stagger: 0.12 });
return (
<section id="timeline" className="py-20 md:py-32 px-[5vw] bg-charcoal">
<div className="max-w-[1200px] mx-auto">
<div className="space-y-4 mb-16">
<p className="section-label">{timelinePage.label}</p>
<p className="text-text-secondary max-w-xl">
{timelinePage.subtitle}
</p>
</div>
<div ref={revealRef} className="relative">
{/* Vertical line */}
<div className="absolute left-4 md:left-1/2 top-0 bottom-0 w-px bg-warm-grey md:-translate-x-px" />
<div className="space-y-12">
{timeline.map((item) => {
const isLeft = item.side === "left";
return (
<div
key={item.year}
className="relative flex items-start md:items-center"
>
{/* Dot */}
<div className="absolute left-4 md:left-1/2 w-3 h-3 bg-amber border-2 border-deep-space -translate-x-1/2 mt-2 md:mt-0 z-10" />
{/* Content */}
<div
className={`pl-12 md:pl-0 md:w-1/2 ${
isLeft
? "md:pr-12 md:text-right"
: "md:pl-12 md:ml-auto"
}`}
>
<div className="space-y-3">
<span className="font-mono text-amber text-sm">
{item.year}
</span>
<h3 className="font-display font-semibold text-text-primary text-xl tracking-tight">
{item.title}
</h3>
<p className="text-text-secondary text-sm leading-relaxed">
{item.description}
</p>
<div
className={`flex flex-wrap gap-2 ${
isLeft ? "md:justify-end" : ""
}`}
>
{item.tags.map((tag) => (
<span key={tag} className="chip">
{tag}
</span>
))}
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}

View file

@ -1,7 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "lib": [
"lib": ["dom", "dom.iterable", "esnext"], "dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@ -11,7 +14,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@ -19,9 +22,22 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": [
} "./src/*"
]
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "target": "ES2017"
"exclude": ["node_modules"] },
"include": [
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next-env.d.ts",
"dist/types/**/*.ts",
".next/dev/types/**/*.ts",
"dist/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }