Files
MarvinKrausser.github.io/index.html
T
2026-03-18 14:12:31 +01:00

602 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Portfolio</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #fafaf8;
--surface: #f3f3f0;
--border: #e5e5e0;
--text: #1a1a18;
--muted: #999990;
--serif: 'Instrument Serif', Georgia, serif;
--sans: 'DM Sans', system-ui, sans-serif;
--nav-w: 230px;
}
@media (prefers-color-scheme: dark) {
:root { --bg:#141412; --surface:#1c1c19; --border:#2a2a26; --text:#f0f0ec; --muted:#686860; }
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg); color: var(--text);
font-family: var(--sans); font-size: 15px;
font-weight: 300; line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
/* ── SIDEBAR ─────────────────────────────────────── */
.sidebar {
position: fixed; top: 0; left: 0;
width: var(--nav-w); height: 100vh;
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
z-index: 100; background: var(--bg);
}
.sidebar-top {
padding: 36px 32px 28px;
border-bottom: 1px solid var(--border);
}
.sidebar-name {
font-family: var(--serif); font-size: 1.1rem;
color: var(--text); text-decoration: none; display: block;
margin-bottom: 4px;
}
.sidebar-sub {
font-size: 0.75rem; color: var(--muted);
}
.sidebar-nav {
flex: 1; overflow-y: auto; padding: 20px 0;
}
.nav-group {
margin-bottom: 4px;
}
.nav-group-label {
font-size: 0.65rem; font-weight: 500;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--muted); padding: 16px 32px 6px;
display: block;
}
.nav-link {
display: flex; align-items: center; gap: 10px;
padding: 6px 32px; font-size: 0.84rem; font-weight: 400;
color: var(--muted); text-decoration: none;
transition: color 0.15s; position: relative;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nav-link::before {
content: ''; position: absolute; left: 0; top: 50%;
transform: translateY(-50%); width: 2px; height: 0;
background: var(--text); border-radius: 0 2px 2px 0;
transition: height 0.18s;
}
.nav-link:hover { color: var(--text); }
.nav-link.active { color: var(--text); font-weight: 500; }
.nav-link.active::before { height: 14px; }
.nav-link .idx {
font-size: 0.68rem; color: var(--muted);
min-width: 18px; flex-shrink: 0;
}
.sidebar-bottom {
padding: 20px 32px;
border-top: 1px solid var(--border);
display: flex; flex-direction: column; gap: 6px;
}
.sidebar-bottom a {
font-size: 0.76rem; color: var(--muted);
text-decoration: none; transition: color 0.15s;
}
.sidebar-bottom a:hover { color: var(--text); }
/* ── MAIN ────────────────────────────────────────── */
.main { margin-left: var(--nav-w); }
/* ── EACH PROJECT SECTION ────────────────────────── */
.project-section {
padding: 96px 80px;
border-bottom: 1px solid var(--border);
}
/* Section header */
.section-head {
margin-bottom: 56px;
}
.section-kicker {
font-size: 0.7rem; font-weight: 500;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--muted); margin-bottom: 14px;
display: flex; align-items: center; gap: 10px;
}
.section-kicker::before {
content: ''; display: inline-block;
width: 24px; height: 1px; background: var(--muted);
}
.section-title {
font-family: var(--serif);
font-size: clamp(2rem, 3.5vw, 3rem);
font-weight: 400; letter-spacing: -0.02em; line-height: 1.12;
margin-bottom: 18px;
}
.section-desc {
max-width: 560px; color: var(--muted);
font-size: 0.94rem; line-height: 1.8;
}
/* Tags + meta row */
.meta-row {
display: flex; align-items: center;
gap: 20px; flex-wrap: wrap; margin-top: 20px;
}
.tags { display: flex; flex-wrap: wrap; gap: 6px; }
.tag {
font-size: 0.71rem; color: var(--muted);
background: var(--surface); border: 1px solid var(--border);
padding: 3px 10px; border-radius: 3px;
}
.stat {
font-size: 0.78rem; color: var(--muted);
}
.status-dot {
display: inline-block; width: 6px; height: 6px;
border-radius: 50%; background: currentColor;
vertical-align: middle; margin-right: 4px;
}
.status-dot.active { color: #3a7d44; }
.status-dot.archived { color: var(--muted); opacity: .5; }
/* Content blocks (alternating) */
.content-blocks {
display: flex; flex-direction: column; gap: 0;
}
.content-block {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-top: 1px solid var(--border);
}
/* even blocks: flip columns */
.content-block.flip {
direction: rtl;
}
.content-block.flip > * { direction: ltr; }
.block-image {
aspect-ratio: 4/3;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
overflow: hidden; position: relative;
}
.content-block.flip .block-image {
border-right: none;
border-left: 1px solid var(--border);
}
/* Placeholder visuals (replace with real <img> tags) */
.block-image img {
width: 100%; height: 100%; object-fit: cover; display: block;
}
.placeholder {
width: 100%; height: 100%;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 10px; color: var(--muted);
}
.placeholder-icon { font-size: 2.4rem; line-height: 1; }
.placeholder-label { font-size: 0.72rem; letter-spacing: 0.06em; }
.block-text {
padding: 44px 48px;
display: flex; flex-direction: column; justify-content: center;
}
.block-label {
font-size: 0.68rem; font-weight: 500;
letter-spacing: 0.1em; text-transform: uppercase;
color: var(--muted); margin-bottom: 16px;
}
.block-text h3 {
font-family: var(--serif); font-size: 1.25rem;
font-weight: 400; letter-spacing: -0.01em;
line-height: 1.3; margin-bottom: 14px;
}
.block-text p {
font-size: 0.88rem; color: var(--muted);
line-height: 1.8;
}
/* Links row */
.links-row {
display: flex; gap: 20px; margin-top: 40px;
}
.text-link {
font-size: 0.84rem; font-weight: 500; color: var(--text);
text-decoration: none; border-bottom: 1px solid var(--border);
padding-bottom: 2px; transition: border-color 0.15s;
}
.text-link:hover { border-color: var(--text); }
.text-link.ghost { color: var(--muted); }
/* ── HERO ────────────────────────────────────────── */
.hero-section {
padding: 100px 80px 80px;
border-bottom: 1px solid var(--border);
}
.hero-eyebrow {
font-size: 0.7rem; font-weight: 500;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--muted); margin-bottom: 24px;
}
.hero-section h1 {
font-family: var(--serif);
font-size: clamp(2.8rem, 5vw, 4.2rem);
font-weight: 400; letter-spacing: -0.025em;
line-height: 1.1; margin-bottom: 28px; max-width: 640px;
}
.hero-section h1 em { font-style: italic; color: var(--muted); }
.hero-desc {
max-width: 460px; color: var(--muted);
font-size: 0.94rem; line-height: 1.8; margin-bottom: 40px;
}
.hero-links { display: flex; gap: 24px; flex-wrap: wrap; }
/* ── MOBILE ──────────────────────────────────────── */
.mobile-bar {
display: none; position: sticky; top: 0; z-index: 100;
background: var(--bg); border-bottom: 1px solid var(--border);
padding: 14px 20px; align-items: center; justify-content: space-between;
}
.mobile-logo { font-family: var(--serif); font-size: 1rem; color: var(--text); text-decoration: none; }
.mobile-btn { background: none; border: none; cursor: pointer; color: var(--muted); font-family: var(--sans); font-size: 0.85rem; padding: 4px; }
.mobile-drawer {
display: none; position: fixed; inset: 0; z-index: 200;
background: var(--bg); overflow-y: auto;
padding: 24px 0; flex-direction: column;
}
.mobile-drawer.open { display: flex; }
.mobile-drawer-head {
display: flex; justify-content: space-between; align-items: center;
padding: 0 24px 24px; border-bottom: 1px solid var(--border); margin-bottom: 16px;
}
.mobile-drawer-head span { font-family: var(--serif); font-size: 1rem; }
.mobile-drawer a {
display: block; padding: 10px 24px;
font-size: 0.9rem; color: var(--muted); text-decoration: none;
}
.mobile-drawer a:hover { color: var(--text); }
@media (max-width: 900px) {
.sidebar { display: none; }
.main { margin-left: 0; }
.mobile-bar { display: flex; }
.hero-section, .project-section { padding: 56px 24px; }
.content-block { grid-template-columns: 1fr; }
.content-block.flip { direction: ltr; }
.block-image { aspect-ratio: 16/9; border-right: none; border-bottom: 1px solid var(--border); }
.content-block.flip .block-image { border-left: none; border-bottom: 1px solid var(--border); }
.block-text { padding: 28px 24px; }
}
</style>
</head>
<body>
<!-- MOBILE BAR -->
<div class="mobile-bar">
<a class="mobile-logo" href="#hero">Marvin Kraußer</a>
<button class="mobile-btn" onclick="openDrawer()">Menü ↓</button>
</div>
<div class="mobile-drawer" id="drawer">
<div class="mobile-drawer-head">
<span>Navigation</span>
<button class="mobile-btn" onclick="closeDrawer()">✕ Schließen</button>
</div>
<div id="drawer-links"></div>
</div>
<!-- SIDEBAR -->
<aside class="sidebar">
<div class="sidebar-top">
<a class="sidebar-name" href="#hero">Marvin Kraußer</a>
<span class="sidebar-sub">Student Medieninformatik</span>
</div>
<nav class="sidebar-nav" id="sidebar-nav"></nav>
<div class="sidebar-bottom">
<a href="https://github.com/deinname" target="_blank">github.com/marvinkrausser ↗</a>
<a href="mailto:dein@email.de">marvin.krausser277@gmail.com</a>
</div>
</aside>
<!-- MAIN -->
<main class="main">
<!-- HERO -->
<section id="hero" data-nav="Start" class="hero-section">
<p class="hero-eyebrow">Open-Source Developer</p>
<h1>Dinge bauen,<br><em>die bleiben.</em></h1>
<p class="hero-desc">Ich entwickle Open-Source-Tools, Web-Apps und CLI-Utilities. Jedes Projekt hat eine klare Aufgabe und einen sauberen Code.</p>
<div class="hero-links">
<a class="text-link" href="https://github.com/deinname" target="_blank">GitHub ↗</a>
<a class="text-link ghost" href="#awesome-cli">Projekte ansehen</a>
</div>
</section>
<!-- PROJEKTE werden per JS eingefügt -->
<div id="projects-root"></div>
</main>
<script>
/* ══════════════════════════════════════════════════════
DATEN
Jedes Projekt hat:
- id, title, description, tags, stars, forks, status,
github, demo
- blocks[]: Array von { label, heading, text, image }
image kann ein URL-String sein oder null (= Platzhalter)
══════════════════════════════════════════════════════ */
const PROJECTS = [
{
id: "awesome-cli",
title: "awesome-cli",
description: "CLI-Framework für Node.js mit automatischer Hilfe-Generierung, Farbausgabe und Plugin-System. TypeScript out-of-the-box.",
tags: ["TypeScript", "Node.js", "CLI"],
stars: 342, forks: 28, status: "active",
github: "https://github.com/deinname/awesome-cli",
demo: "https://awesome-cli.dev",
blocks: [
{
label: "Überblick",
heading: "Schnell zum fertigen CLI",
text: "awesome-cli nimmt den Boilerplate-Aufwand weg. Mit wenigen Zeilen entsteht ein vollständiges CLI mit automatisch generierter Hilfe, farbiger Ausgabe und Subcommands.",
image: null,
},
{
label: "Plugin-System",
heading: "Erweiterbar durch Plugins",
text: "Das integrierte Plugin-System erlaubt es, Commands, Middleware und Custom-Renderer zu registrieren – ohne den Core zu modifizieren.",
image: null,
},
{
label: "TypeScript",
heading: "First-class TypeScript-Support",
text: "Vollständige Typ-Definitionen, generische Command-Handler und automatische Completion-Generierung direkt aus den Typen.",
image: null,
},
],
},
{
id: "react-grid-magic",
title: "react-grid-magic",
description: "Drag-and-Drop Grid-Layout-Bibliothek für React mit Resize-Support und responsiven Breakpoints.",
tags: ["React", "CSS Grid", "DnD"],
stars: 118, forks: 9, status: "active",
github: "https://github.com/deinname/react-grid-magic",
demo: "https://react-grid-magic.netlify.app",
blocks: [
{
label: "Layout",
heading: "Flexibles Grid-System",
text: "Definiere dein Layout deklarativ. Breakpoints werden automatisch berechnet, Abstände bleiben konsistent – egal wie viele Items im Grid liegen.",
image: null,
},
{
label: "Interaktion",
heading: "Drag, Drop & Resize",
text: "Nutzer können Elemente per Drag-and-Drop verschieben und in der Größe anpassen. Alle Events sind vollständig kontrollierbar.",
image: null,
},
],
},
{
id: "dataflow-py",
title: "dataflow-py",
description: "Python-Bibliothek für deklarative Datenpipelines. Inspiriert von RxJS, aber für datenwissenschaftliche Workflows.",
tags: ["Python", "Pandas", "async"],
stars: 87, forks: 12, status: "active",
github: "https://github.com/deinname/dataflow-py",
demo: null,
blocks: [
{
label: "Konzept",
heading: "Pipelines statt Loops",
text: "Statt imperative Schleifen zu schreiben, beschreibst du den Datenfluss deklarativ. dataflow-py kümmert sich um Parallelisierung und Fehlerbehandlung.",
image: null,
},
{
label: "Async",
heading: "Nativ asynchron",
text: "Jede Pipeline kann async ausgeführt werden. Quellen, Transformer und Senken sind frei kombinierbar – auch mit bestehenden Pandas-Workflows.",
image: null,
},
],
},
{
id: "vault-rs",
title: "vault-rs",
description: "Kommandozeilen-Passwortmanager in Rust. Lokale Verschlüsselung, portabel, kein Cloud-Zwang.",
tags: ["Rust", "Crypto", "CLI"],
stars: 173, forks: 21, status: "active",
github: "https://github.com/deinname/vault-rs",
demo: null,
blocks: [
{
label: "Sicherheit",
heading: "Verschlüsselung lokal, nicht in der Cloud",
text: "Alle Daten liegen AES-256-verschlüsselt auf deinem Gerät. Kein Server, kein Account, kein Tracking.",
image: null,
},
{
label: "Portabilität",
heading: "Ein Binary, überall lauffähig",
text: "vault-rs kompiliert zu einem einzelnen, statisch gelinkten Binary. Kein Runtime, keine Abhängigkeiten – einfach kopieren und loslegen.",
image: null,
},
],
},
];
/* ══════════════════════════════════════════════════════
WEB COMPONENT: <project-section>
Rendert einen kompletten Projekt-Abschnitt mit
Header, alternierenden Content-Blocks und Link-Zeile.
══════════════════════════════════════════════════════ */
class ProjectSection extends HTMLElement {
connectedCallback() {
const p = JSON.parse(this.getAttribute("data-project") || "{}");
if (!p.id) return;
const tags = p.tags.map(t => `<span class="tag">${t}</span>`).join("");
const demo = p.demo
? `<a class="text-link ghost" href="${p.demo}" target="_blank">Live-Demo ↗</a>`
: "";
const blocks = (p.blocks || []).map((b, i) => {
const flip = i % 2 === 1 ? " flip" : "";
const imageContent = b.image
? `<img src="${b.image}" alt="${b.heading}" loading="lazy" />`
: `<div class="placeholder">
<div class="placeholder-icon">◻</div>
<div class="placeholder-label">Bild einfügen</div>
</div>`;
return `
<div class="content-block${flip}">
<div class="block-image">${imageContent}</div>
<div class="block-text">
<p class="block-label">${b.label}</p>
<h3>${b.heading}</h3>
<p>${b.text}</p>
</div>
</div>`;
}).join("");
this.innerHTML = `
<section class="project-section" id="${p.id}" data-nav="${p.title}">
<div class="section-head">
<p class="section-kicker">${p.title}</p>
<h2 class="section-title">${p.title}</h2>
<p class="section-desc">${p.description}</p>
<div class="meta-row">
<div class="tags">${tags}</div>
<span class="stat">★ ${p.stars}</span>
<span class="stat">⑂ ${p.forks}</span>
<span class="stat">
<span class="status-dot ${p.status}"></span>
${p.status === "active" ? "Aktiv" : "Archiviert"}
</span>
</div>
</div>
<div class="content-blocks">${blocks}</div>
<div class="links-row">
<a class="text-link" href="${p.github}" target="_blank">Auf GitHub ansehen ↗</a>
${demo}
</div>
</section>`;
}
}
customElements.define("project-section", ProjectSection);
/* ══════════════════════════════════════════════════════
RENDER
══════════════════════════════════════════════════════ */
const root = document.getElementById("projects-root");
PROJECTS.forEach(p => {
const el = document.createElement("project-section");
el.setAttribute("data-project", JSON.stringify(p));
root.appendChild(el);
});
/* ══════════════════════════════════════════════════════
NAV AUFBAUEN + ACTIVE STATE
══════════════════════════════════════════════════════ */
function buildNav() {
const sidebarNav = document.getElementById("sidebar-nav");
const drawerLinks = document.getElementById("drawer-links");
sidebarNav.innerHTML = "";
drawerLinks.innerHTML = "";
// Statische Sections
const staticSections = [
{ id: "hero", label: "Start", group: null },
];
// Projekt-Sections nach dem Rendern einsammeln
const projectSections = PROJECTS.map((p, i) => ({
id: p.id,
label: p.title,
idx: String(i + 1).padStart(2, "0"),
group: "Projekte",
}));
const allSections = [...staticSections, ...projectSections];
// Gruppen
let lastGroup = "__none__";
allSections.forEach(s => {
if (s.group !== lastGroup) {
lastGroup = s.group;
if (s.group) {
const lbl = document.createElement("span");
lbl.className = "nav-group-label";
lbl.textContent = s.group;
sidebarNav.appendChild(lbl);
}
}
// Sidebar link
const a = document.createElement("a");
a.className = "nav-link";
a.href = `#${s.id}`;
a.dataset.target = s.id;
if (s.idx) {
const num = document.createElement("span");
num.className = "idx"; num.textContent = s.idx;
a.appendChild(num);
}
a.appendChild(document.createTextNode(s.label));
sidebarNav.appendChild(a);
// Mobile drawer link
const ma = document.createElement("a");
ma.href = `#${s.id}`;
ma.textContent = s.idx ? `${s.idx} – ${s.label}` : s.label;
ma.onclick = closeDrawer;
drawerLinks.appendChild(ma);
});
}
function setupObserver() {
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
document.querySelectorAll(".nav-link").forEach(a =>
a.classList.toggle("active", a.dataset.target === e.target.id)
);
}
});
}, { rootMargin: "0px 0px -55% 0px", threshold: 0 });
document.querySelectorAll("[data-nav]").forEach(s => observer.observe(s));
}
// Custom elements render async – short timeout before building nav
setTimeout(() => { buildNav(); setupObserver(); }, 120);
/* ══════════════════════════════════════════════════════
MOBILE DRAWER
══════════════════════════════════════════════════════ */
function openDrawer() { document.getElementById("drawer").classList.add("open"); }
function closeDrawer() { document.getElementById("drawer").classList.remove("open"); }
</script>
</body>
</html>