Compare commits

..
13 Commits
Author SHA1 Message Date
marvin 498ac6e1a4 fixed typos 2026-09-25 07:47:29 +02:00
marvin 60f3354e35 changed docker network 2026-09-25 07:30:12 +02:00
marvin 03e23f9802 added gitea entry 2026-09-25 07:22:03 +02:00
marvin 43203af33e changed website link path 2026-09-22 17:26:59 +02:00
marvin e0c424286f upgraded configs 2026-09-22 16:30:35 +02:00
marvin 57ddd4f6a2 added missing tech tag 2026-09-21 21:41:08 +02:00
marvin dfa253bf48 split up css style files 2026-09-21 21:40:11 +02:00
marvin e38f26e289 added readme 2026-09-21 21:31:50 +02:00
marvin cc91f7d4ff split javascript file into multiple 2026-09-21 21:25:31 +02:00
marvin b09f2029ed changed career section style 2026-09-21 21:07:37 +02:00
marvin af21ce0fca changed siemens title 2026-09-21 18:41:10 +02:00
marvin 524c9dc295 added career section 2026-09-21 09:32:28 +02:00
marvin afafa911d2 changed navbar width 2026-09-21 05:57:05 +02:00
46 changed files with 1440 additions and 966 deletions
+5
View File
@@ -0,0 +1,5 @@
.git
.dockerignore
Dockerfile
compose.yaml
README.md
+3
View File
@@ -4,6 +4,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY . /usr/share/nginx/html COPY . /usr/share/nginx/html
# nginx.conf is needed in the build context but must not be served
RUN rm /usr/share/nginx/html/nginx.conf
EXPOSE 8081 EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+130
View File
@@ -0,0 +1,130 @@
# Marvin Kraußer – Portfolio
Personal portfolio website of Marvin Kraußer, Media Informatics student (B.Sc.) at Ulm University and working student in software engineering at Siemens Mobility.
**Live:** https://marvinkrausser.github.io
The site is built with plain HTML, CSS and JavaScript. It has no framework, no build step and no runtime dependencies.
## Highlights
- **Content-driven:** all texts, projects, career entries and tags live in language-specific JSON files (`lang/en.json`, `lang/de.json`). The page is generated from them at load time, so adding a project or a job means editing JSON, not markup.
- **Bilingual (EN / DE):** the language is picked from a cookie, falling back to the browser language, and can be switched at any time. Dates are formatted per locale with `Intl.DateTimeFormat`.
- **Light / dark theme:** the choice is persisted in a cookie.
- **Responsive:** a fixed sidebar on desktop and a slide-in navigation on small screens and touch devices.
- **Navigation:** a scroll spy highlights the section currently in view, and the scroll position is restored after a reload or language switch.
- **Lightweight:** lazy-loaded images, no external libraries, no tracking.
## Featured projects
| Project | Stack | Repository |
| --- | --- | --- |
| CNN Website – bird classification and real-time face detection, served from a containerised backend | React, PyTorch, NGINX, Caddy, Docker Compose | [CNN_Website](https://github.com/MarvinKrausser/CNN_Website) |
| PixelCNN Generator – autoregressive image generation with a seminar paper | Python, PyTorch, CUDA, LaTeX | [pixelcnn](https://github.com/MarvinKrausser/pixelcnn) |
| Online Multiplayer Board Game – server, Unity client and AI players, built with five other students using Scrum | C#, .NET, Unity, Docker | [BoardgameServer](https://github.com/MarvinKrausser/BoardgameServer) |
| Tower Defense Game – single-player game with level editor and A\* pathfinding | C#, Unity | [TowerDefenseGame](https://github.com/MarvinKrausser/TowerDefenseGame) |
## How it works
`index.html` contains only the static shell: sidebar, mobile navigation and empty containers. On load, `scripts/main.js` picks the language, fetches `lang/<lang>.json` and `config/img_config.json`, and renders everything else.
The JavaScript is split into small ES modules, each with one responsibility:
```
scripts/
main.js entry point: wires up the controls, loads data, renders
core/
cookies.js cookie helpers
dom.js small element-creation helper
dates.js locale-aware date formatting
data.js loading of language and image config files
ui/
theme.js light / dark toggle
language.js language detection and dropdown
navbar.js sidebar and mobile navigation
scroll.js scroll restoration and scroll spy
sections/
common.js shared section skeleton (header, tags)
sidebar.js status lines under the name
introduction.js
work.js career entries
projects.js project entries with image / text blocks
```
The stylesheets are organised the same way. `styles/main.css` imports the individual files in cascade order:
```
styles/
main.css entry point, imports everything below
base/
variables.css colours, fonts and sizes as CSS custom properties (light and dark)
base.css resets and typography
utilities.css helper classes
layout/
sidebar.css fixed desktop sidebar
navbar.css desktop navigation links
mobile_bar.css slide-in mobile navigation
main_content.css page margins and breakpoints
components/
theme_switch.css light / dark toggle
lang_dropdown.css language picker
mobile_menu_button.css
tags.css
text_link.css
sections/
introduction.css
section_header.css shared header of work and project sections
project_blocks.css image / text blocks
work.css career entries
```
Colours, spacing and fonts are CSS custom properties, and dark mode is a single class that overrides them.
## Project structure
```
.
├── index.html static shell
├── lang/ en.json, de.json – all page content
├── config/ img_config.json – image ids, paths and crop positions
├── scripts/ ES modules (see above)
├── styles/ stylesheets (see above)
├── images/, images_jpg/ screenshots and diagrams for the projects
├── pdfs/ seminar paper, talk slides, user and developer handbooks
├── Dockerfile NGINX image serving the site
├── compose.yaml Compose service definition
└── nginx.conf
```
## Run locally
The page loads its content with `fetch` and uses ES modules, so it has to be served over HTTP. Opening `index.html` directly from disk does not work.
```bash
git clone https://github.com/MarvinKrausser/MarvinKrausser.github.io.git
cd MarvinKrausser.github.io
python3 -m http.server 8000
```
Then open http://localhost:8000.
### With Docker
```bash
docker build -t portfolio_website .
docker run --rm -p 8081:8081 portfolio_website
```
The site is then available at http://localhost:8081. `compose.yaml` runs the same image as a service on an external Docker network (`cnn_network`), so it can sit behind a reverse proxy next to the other projects.
## Adding content
1. **Project:** add an entry to `content_projects` in both `lang/en.json` and `lang/de.json`. Each block references an image id from `config/img_config.json`. Use `--link--` inside a block's text to place an inline link from its `links` list.
2. **Career entry:** add an entry to `content_work` with its role, dates, tasks and tech tags.
The sidebar and mobile navigation entries are generated automatically.
## Development notes
- The repository follows a feature-branch workflow, so `main` stays deployable.
- No build tooling is involved, so a change to a file is visible on the next reload.
+2 -2
View File
@@ -7,10 +7,10 @@ services:
- "8081" - "8081"
restart: unless-stopped restart: unless-stopped
networks: networks:
- cnn_network - portfolio_network
pull_policy: never pull_policy: never
container_name: portfolio_website container_name: portfolio_website
networks: networks:
cnn_network: portfolio_network:
external: true external: true
+5
View File
@@ -54,6 +54,11 @@
"id": "cnn_website_4", "id": "cnn_website_4",
"image": "images_jpg/yolo.jpg", "image": "images_jpg/yolo.jpg",
"image_pos": "center" "image_pos": "center"
},
{
"id": "selfhost_1",
"image": "images_jpg/gitea.jpg",
"image_pos": "left"
} }
] ]
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

+2 -5
View File
@@ -5,10 +5,7 @@
<link rel="icon" href="images/generated_two_no_background.png"> <link rel="icon" href="images/generated_two_no_background.png">
<meta charset="UTF-8"> <meta charset="UTF-8">
<title id="website_title"></title> <title id="website_title"></title>
<link rel="stylesheet" href="styles/main_style.css"> <link rel="stylesheet" href="styles/main.css">
<link rel="stylesheet" href="styles/sidebar_style.css">
<link rel="stylesheet" href="styles/mobile_bar_style.css">
<link rel="stylesheet" href="styles/projects_style.css">
</head> </head>
<body> <body>
@@ -78,7 +75,7 @@
<div id="projects-section"></div> <div id="projects-section"></div>
</div> </div>
<script src="scripts/main_page.js"></script> <script type="module" src="scripts/main.js"></script>
</body> </body>
+71 -7
View File
@@ -1,13 +1,51 @@
{ {
"title_website": "Marvins Protfolio", "title_website": "Marvins Portfolio",
"title_introduction": "Hallo!", "title_introduction": "Hallo!",
"content_introduction": "Ich studiere Medieninformatik im Bachelor und habe im Rahmen meines Studiums praktische Erfahrungen in verschiedenen Projekten gesammelt. Besonders gerne arbeite ich an Projekten, in denen ich neue Technologien ausprobieren und vertiefen kann, aktuell vor allem im Bereich Machine Learning.", "content_introduction": "Ich studiere Medieninformatik im Bachelor und habe im Rahmen meines Studiums praktische Erfahrungen in verschiedenen Projekten gesammelt. Besonders gerne arbeite ich an Projekten, in denen ich neue Technologien ausprobieren und vertiefen kann, aktuell vor allem im Bereich Machine Learning.",
"status": "Student B.Sc.", "status": "Student B.Sc.",
"profession": "Medieninformatik", "profession": "Medieninformatik",
"workplace": "Universität Ulm", "workplace": "Universität Ulm",
"github_desc": "Auf GitHub ansehen ↗", "github_desc": "Auf GitHub ansehen ↗",
"navbar_group": "Projekte", "navbar_group_work": "Berufserfahrung",
"content": [ "navbar_group_projects": "Projekte",
"content_work": [
{
"id": "siemens_mobility",
"title": "Siemens Mobility",
"company": "Siemens Mobility GmbH",
"description": "Werkstudent Softwareentwicklung in Simulationsumgebung (Hybrid)",
"type": "Hybrid",
"tasks": [
{
"title": "Netzwerkentwicklung & Integration",
"content": "Konzeption und Implementierung einer stabilen Remote-Schnittstelle zur Anbindung externer Clients an eine Unity-Simulationsumgebung."
},
{
"title": "Server-Infrastruktur & Deployment",
"content": "Einrichtung, Absicherung und Administration eines Debian 13-Servers sowie Bereitstellung der Anwendungsinstanzen."
},
{
"title": "Agile Teamarbeit & Quality Assurance",
"content": "Aktive Kollaboration im Entwicklerteam unter Anwendung von Feature-Branching-Workflows (Git) und systematischen Code-Reviews."
}
],
"tech": [
"Unity",
"WebRTC",
"Linux",
"GitLab",
"C#",
"FFmpeg"
],
"start": "2026-05-15",
"end": "heute",
"locale": "de-DE",
"from": "von",
"to": "bis",
"at": "bei"
}
],
"content_projects": [
{ {
"id": "cnn_website", "id": "cnn_website",
"title": "CNN Website", "title": "CNN Website",
@@ -23,12 +61,12 @@
"blocks": [ "blocks": [
{ {
"heading": "Frontend", "heading": "Frontend",
"text": "Die --link-- Website ist online. Das Frontend wurde mit React implementiert und ermöglicht das Hochladen von Bildern sowie die Anzeige von Klassifikationsergebnissen. Es ist responsiv gestaltet und passt sich an verschiedene Bildschirmgrößen an, wodurch es auch auf Mobilgeräten gut nutzbar ist.", "text": "Die --link-- ist online. Das Frontend wurde mit React implementiert und ermöglicht das Hochladen von Bildern sowie die Anzeige von Klassifikationsergebnissen. Es ist responsiv gestaltet und passt sich an verschiedene Bildschirmgrößen an, wodurch es auch auf Mobilgeräten gut nutzbar ist.",
"image": "cnn_website_1", "image": "cnn_website_1",
"links": [ "links": [
{ {
"id": "website", "id": "website",
"link": "https://marvinkrausser.com/bird_cnn", "link": "https://marvinkrausser.com",
"desc": "Website ↗" "desc": "Website ↗"
} }
] ]
@@ -46,17 +84,43 @@
] ]
}, },
{ {
"heading": "Bird Classification", "heading": "Vogelklassifikation",
"text": "Das CNN klassifiziert sieben Vogelarten aus Bildern. Es verwendet Skip Connections, Dropout und Batch Normalization, um das Training stabiler zu machen und Overfitting zu reduzieren.", "text": "Das CNN klassifiziert sieben Vogelarten aus Bildern. Es verwendet Skip Connections, Dropout und Batch Normalization, um das Training stabiler zu machen und Overfitting zu reduzieren.",
"image": "cnn_website_3" "image": "cnn_website_3"
}, },
{ {
"heading": "Face Detection", "heading": "Gesichtserkennung",
"text": "Das Modell basiert auf der YOLO-Architektur und kann Gesichter in Echtzeit erkennen, selbst auf schwacher Hardware (z. B. ein Server mit 1 GB RAM und 1 vCPU). Das Frontend sendet kontinuierlich ausgeschnittene Frames des Webcam-Streams über WebSockets an den Server.", "text": "Das Modell basiert auf der YOLO-Architektur und kann Gesichter in Echtzeit erkennen, selbst auf schwacher Hardware (z. B. ein Server mit 1 GB RAM und 1 vCPU). Das Frontend sendet kontinuierlich ausgeschnittene Frames des Webcam-Streams über WebSockets an den Server.",
"image": "cnn_website_4" "image": "cnn_website_4"
} }
] ]
}, },
{
"id": "selfhosting",
"title": "Self-Hosting",
"description": "Selbst gehostete Dienste, die in Docker-Containern auf meinem eigenen Debian 13-Server laufen.",
"tags": [
"Linux",
"Docker Compose",
"Gitea",
"SQLite"
],
"github": "https://git.marvinkrausser.com",
"blocks": [
{
"heading": "Gitea",
"text": "Die --link-- dient mir als privater Git-Server für persönliche Repositories. Sie läuft als Docker-Container, der nur über den Caddy Reverse Proxy erreichbar ist, welcher HTTPS mit Let's-Encrypt-Zertifikaten bereitstellt. Der Git-Zugriff per SSH läuft über einen separaten Port und funktioniert ausschließlich mit SSH-Keys. Alle Daten, einschließlich der Repositories und der SQLite-Datenbank, liegen in einem einzigen Ordner auf dem Host, sodass ein Backup lediglich aus dem Archivieren dieses Ordners besteht.",
"image": "selfhost_1",
"links": [
{
"id": "gitea",
"link": "https://git.marvinkrausser.com",
"desc": "Gitea-Instanz ↗"
}
]
}
]
},
{ {
"id": "pixelcnn", "id": "pixelcnn",
"title": "PixelCNN Bildgeneration", "title": "PixelCNN Bildgeneration",
+67 -3
View File
@@ -6,8 +6,46 @@
"profession": "Media Informatics", "profession": "Media Informatics",
"workplace": "Ulm University", "workplace": "Ulm University",
"github_desc": "View on GitHub ↗", "github_desc": "View on GitHub ↗",
"navbar_group": "Projects", "navbar_group_work": "Career",
"content": [ "navbar_group_projects": "Projects",
"content_work": [
{
"id": "siemens_mobility",
"title": "Siemens Mobility",
"company": "Siemens Mobility GmbH",
"description": "Working Student Software Engineering in Simulation Environment (Hybrid)",
"type": "Hybrid",
"tasks": [
{
"title": "Network Development & Integration",
"content": "Design and implementation of a stable remote interface to connect external clients to a Unity simulation environment."
},
{
"title": "Server Infrastructure & Deployment",
"content": "Setup, securing, and administration of a Debian 13 server as well as deployment of application instances."
},
{
"title": "Agile Teamwork & Quality Assurance",
"content": "Active collaboration within the development team using feature-branching workflows (Git) and systematic code reviews."
}
],
"tech": [
"Unity",
"WebRTC",
"Linux",
"GitLab",
"C#",
"FFmpeg"
],
"start": "2026-05-15",
"end": "present",
"locale": "en-US",
"from": "from",
"to": "to",
"at": "at"
}
],
"content_projects": [
{ {
"id": "cnn_website", "id": "cnn_website",
"title": "CNN Website", "title": "CNN Website",
@@ -28,7 +66,7 @@
"links": [ "links": [
{ {
"id": "website", "id": "website",
"link": "https://marvinkrausser.com/bird_cnn", "link": "https://marvinkrausser.com",
"desc": "Website ↗" "desc": "Website ↗"
} }
] ]
@@ -57,6 +95,32 @@
} }
] ]
}, },
{
"id": "selfhosting",
"title": "Self-Hosting",
"description": "Self-hosted services running in Docker containers on my own Debian 13 server.",
"tags": [
"Linux",
"Docker Compose",
"Gitea",
"SQLite"
],
"github": "https://git.marvinkrausser.com",
"blocks": [
{
"heading": "Gitea",
"text": "The --link-- serves as my private Git server for personal repositories. It runs as a Docker container that is only reachable through the Caddy reverse proxy, which provides HTTPS with Let's Encrypt certificates. Git access via SSH uses a separate port and works exclusively with SSH keys. All data, including repositories and the SQLite database, is stored in a single folder on the host, making backups as simple as archiving that folder.",
"image": "selfhost_1",
"links": [
{
"id": "gitea",
"link": "https://git.marvinkrausser.com",
"desc": "Gitea instance ↗"
}
]
}
]
},
{ {
"id": "pixelcnn", "id": "pixelcnn",
"title": "PixelCNN Generator", "title": "PixelCNN Generator",
+20
View File
@@ -1,6 +1,26 @@
server { server {
listen 8081; listen 8081;
server_name localhost; server_name localhost;
server_tokens off;
root /usr/share/nginx/html; root /usr/share/nginx/html;
# Compression
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Never serve dotfiles (.git, .dockerignore, ...)
location ~ /\. {
deny all;
}
# Images and PDFs rarely change. HTML, JS, CSS and JSON are not fingerprinted,
# so they keep nginx's default (ETag / Last-Modified revalidation).
location ~* \.(jpg|png|pdf)$ {
expires 30d;
}
} }
+14
View File
@@ -0,0 +1,14 @@
export function setCookie(name, value, maxAge) {
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`;
}
export function getCookie(name) {
const cookies = document.cookie.split("; ");
for (let c of cookies) {
const [key, value] = c.split("=");
if (key === name) return value;
}
return null;
}
+9
View File
@@ -0,0 +1,9 @@
export async function loadContent(lang) {
const response = await fetch(`./lang/${lang}.json`);
return response.json();
}
export async function loadImageConfig() {
const response = await fetch("./config/img_config.json");
return response.json();
}
+19
View File
@@ -0,0 +1,19 @@
export function formatDate(date, locale) {
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) {
return date;
}
return new Intl.DateTimeFormat(locale || "de-DE", {
dateStyle: "medium"
}).format(dateObj);
}
// "from <start> to <end>", with the wording taken from the language file.
export function formatDateRange(item) {
const startDate = formatDate(item.start, item.locale);
const endDate = formatDate(item.end, item.locale);
return item.from + "\n" + startDate + "\n" + item.to + "\n" + endDate;
}
+7
View File
@@ -0,0 +1,7 @@
// Creates an element with an optional class name and text content.
export function el(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
}
+41
View File
@@ -0,0 +1,41 @@
import { loadContent, loadImageConfig } from "./core/data.js";
import { detectLanguage, setupLanguageDropdown } from "./ui/language.js";
import { setupThemeToggle } from "./ui/theme.js";
import { createNavbar, setupMobileNavbar, hideMobileNavbar } from "./ui/navbar.js";
import { setupScrollRestoration, restoreScroll, setupScrollSpy } from "./ui/scroll.js";
import { renderSidebarTop } from "./sections/sidebar.js";
import { renderIntroduction } from "./sections/introduction.js";
import { renderWork } from "./sections/work.js";
import { renderProjects } from "./sections/projects.js";
const lang = detectLanguage();
// Each control exists twice: sidebar (desktop) and mobile bar.
setupThemeToggle("themeToggle", ["themeToggle2"]);
setupThemeToggle("themeToggle2", ["themeToggle"]);
setupLanguageDropdown(lang, "lang-button", "lang-menu", "lang-dropdown");
setupLanguageDropdown(lang, "lang-button2", "lang-menu2", "lang-dropdown2");
setupScrollRestoration();
async function init() {
const data = await loadContent(lang);
const imageConfig = await loadImageConfig();
document.getElementById("website_title").textContent = data.title_website;
setupMobileNavbar();
const navbar = createNavbar();
const container = document.getElementById("projects-section");
renderSidebarTop(data);
renderIntroduction(data, navbar);
renderWork(container, navbar, data);
renderProjects(container, navbar, data, imageConfig);
restoreScroll();
setupScrollSpy();
hideMobileNavbar();
}
init();
-367
View File
@@ -1,367 +0,0 @@
let lang = getCookie("lang");
if (lang == null) {
lang = navigator.language.split("-")[0];
}
if (lang != "en" && lang != "de") {
lang = "en";
}
document.documentElement.lang = lang;
function setupLangDropdown(buttonId, menuId, dropdownId, themeToggle, themeToggleOthers) {
const toggle = document.getElementById(themeToggle);
const toggleElements = themeToggleOthers.map(id => document.getElementById(id));
toggle.addEventListener("change", () => {
document.documentElement.classList.toggle("dark", toggle.checked);
if (toggle.checked) {
setCookie("theme", "dark", 60 * 60 * 24 * 7);
toggleElements.forEach(element => {
element.checked = true;
});
}
else {
setCookie("theme", "light", 60 * 60 * 24 * 7);
toggleElements.forEach(element => {
element.checked = false;
});
}
});
const theme = getCookie("theme");
if (theme === "dark") {
document.documentElement.classList.add("dark");
toggle.checked = true;
}
const button = document.getElementById(buttonId);
const menu = document.getElementById(menuId);
const dropdown = document.getElementById(dropdownId);
const items = menu.querySelectorAll(".lang-item");
button.textContent = lang;
button.addEventListener("click", (e) => {
e.stopPropagation();
items.forEach(item => {
item.classList.toggle("hidden", item.dataset.lang == lang);
});
menu.classList.toggle("hidden");
dropdown.classList.toggle("show-border");
});
items.forEach(item => {
item.addEventListener("click", () => {
const lang_selected = item.dataset.lang;
button.textContent = item.textContent;
menu.classList.add("hidden");
dropdown.classList.remove("show-border");
items.forEach(el => el.classList.add("hidden"));
setCookie("lang", lang_selected, "3600");
location.reload();
});
});
document.addEventListener("click", (e) => {
if (!e.target.closest(`#${dropdownId}`)) {
menu.classList.add("hidden");
dropdown.classList.remove("show-border");
items.forEach(el => el.classList.add("hidden"));
}
});
}
setupLangDropdown("lang-button", "lang-menu", "lang-dropdown", "themeToggle", ["themeToggle2"]);
setupLangDropdown("lang-button2", "lang-menu2", "lang-dropdown2", "themeToggle2", ["themeToggle"]);
history.scrollRestoration = "manual";
function saveScroll() {
sessionStorage.setItem("scrollY", window.scrollY);
}
function restoreScroll() {
const scrollY = sessionStorage.getItem("scrollY");
if (scrollY !== null) {
document.documentElement.style.scrollBehavior = "auto";
window.scrollTo(0, parseInt(scrollY));
document.documentElement.style.scrollBehavior = "smooth";
}
}
window.addEventListener("beforeunload", saveScroll);
function setupObserver() {
const sections = document.querySelectorAll("[data-nav]");
const navLinks = document.querySelectorAll(".nav-link");
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
navLinks.forEach(link => {
link.classList.toggle(
"active",
link.dataset.target === entry.target.id
);
});
}
});
}, {
threshold: 0,
rootMargin: "-10% 0px -80% 0px"
});
sections.forEach(section => observer.observe(section));
}
async function loadData() {
const response = await fetch(`./lang/${lang}.json`);
const image_response = await fetch(`./config/img_config.json`)
const image_config = await image_response.json()
const data = await response.json();
const website_title = document.getElementById("website_title");
website_title.textContent = data.title_website;
const introduction = document.getElementById("introduction");
const container = document.getElementById("projects-section");
const navbar = document.getElementById("navbar");
const sidebar_top = document.getElementById("sidebar-top-content");
const navbar_mobile = document.getElementById("navbar-mobile");
const button_mobile2 = document.getElementById("button-mobile");
document.addEventListener("click", (e) => {
if (!e.target.closest("#navbar-mobile") && !e.target.closest("#button-mobile") && !navbar_mobile.classList.contains("inactive")) {
navbar_mobile.classList.add("inactive");
button_mobile2.classList.remove("inactive");
}
});
const navbar_mobile_links = document.getElementById("navbar-mobile-links");
const button_navbar_mobile = document.createElement("button");
const button_mobile = document.getElementById("button-mobile");
button_navbar_mobile.textContent = "";
button_mobile.textContent = "";
button_navbar_mobile.classList = "button-mobile navbar";
navbar_mobile_links.appendChild(button_navbar_mobile);
button_navbar_mobile.addEventListener("click", () => {
navbar_mobile.classList.toggle("inactive");
button_mobile.classList.toggle("inactive");
});
button_mobile.addEventListener("click", () => {
navbar_mobile.classList.toggle("inactive");
button_mobile.classList.toggle("inactive");
});
const sidebar_top_status = document.createElement("p");
sidebar_top_status.textContent = data.status;
sidebar_top.appendChild(sidebar_top_status);
const sidebar_top_profession = document.createElement("p");
sidebar_top_profession.textContent = data.profession;
sidebar_top.appendChild(sidebar_top_profession);
const sidebar_top_workplace = document.createElement("p");
sidebar_top_workplace.textContent = data.workplace;
sidebar_top.appendChild(sidebar_top_workplace);
const headline_introduction = document.createElement("h1");
headline_introduction.textContent = data.title_introduction;
introduction.appendChild(headline_introduction);
const content_introduction = document.createElement("p");
content_introduction.textContent = data.content_introduction;
introduction.appendChild(content_introduction);
const github_link_introduction = document.createElement("a");
github_link_introduction.textContent = "GitHub ↗"
github_link_introduction.className = "text-link introduction";
github_link_introduction.href = "https://github.com/marvinkrausser";
github_link_introduction.target = "_blank";
github_link_introduction.rel = "noopener noreferrer";
introduction.appendChild(github_link_introduction);
create_navbar_entry(navbar, navbar_mobile_links, data.title_introduction, "introduction", 0);
const navbar_group = document.createElement("li");
navbar_group.className = "navbar-group";
navbar_group.textContent = data.navbar_group
navbar.appendChild(navbar_group);
data.content.forEach((item, j) => {
const project = document.createElement("section");
project.className = "project-section";
project.id = item.id;
project.setAttribute("data-nav", item.id);
container.appendChild(project);
create_navbar_entry(navbar, navbar_mobile_links, item.title, item.id, j + 1);
const header = document.createElement("div");
header.className = "section-header";
project.appendChild(header);
const headerTitle = document.createElement("h1");
headerTitle.className = "section-title";
headerTitle.textContent = item.title;
header.appendChild(headerTitle);
const headerDesc = document.createElement("p");
headerDesc.className = "section-desc";
headerDesc.textContent = item.description;
header.appendChild(headerDesc);
const tags = document.createElement("div");
tags.className = "tags";
header.appendChild(tags);
item.tags.forEach(tag_json => {
const tag = document.createElement("span");
tag.className = "tag";
tag.textContent = tag_json;
tags.appendChild(tag);
});
const content_blocks = document.createElement("div");
content_blocks.className = "content-blocks";
project.appendChild(content_blocks);
item.blocks.forEach((content, i) => {
const content_block = document.createElement("div");
content_block.className = "content-block";
content_blocks.appendChild(content_block);
const block_image = document.createElement("div");
const image = document.createElement("img");
const image_id = content.image;
const image_object = image_config.images.find(img => img.id === image_id);
image.className = image_object.image_pos;
image.src = image_object.image;
image.loading = "lazy";
block_image.appendChild(image);
const block_text = document.createElement("div");
const text_title = document.createElement("h2");
text_title.textContent = content.heading;
block_text.appendChild(text_title);
const parts = content.text.split("--link--");
const text_content = document.createElement("div");
text_content.className = "text-content";
block_text.appendChild(text_content);
parts.forEach((part, i) => {
const text = document.createElement("span");
text.textContent = part;
text_content.appendChild(text);
if (i < parts.length - 1) {
const link_list = content.links;
const a = document.createElement("a");
a.href = link_list[i].link;
a.textContent = link_list[i].desc;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.className = "text-link";
text_content.appendChild(a);
}
});
if (i % 2 == 1) {
block_image.className = "block-image right";
block_text.className = "block-text left";
content_block.appendChild(block_text);
content_block.appendChild(block_image);
} else {
block_image.className = "block-image left";
block_text.className = "block-text right";
content_block.appendChild(block_image);
content_block.appendChild(block_text);
}
});
const github_link = document.createElement("div");
github_link.className = "link-row";
project.appendChild(github_link);
const link = document.createElement("a");
link.href = item.github;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.className = "text-link";
link.textContent = data.github_desc;
github_link.appendChild(link);
});
restoreScroll();
setupObserver();
navbar_mobile.classList.add("inactive");
}
loadData();
function create_navbar_entry(navbar, navbar_mobile_links, title, id, j) {
const navbar_item = document.createElement("li");
navbar_item.className = "navbar-item";
navbar.appendChild(navbar_item);
const navbar_link = document.createElement("a");
navbar_link.className = "nav-link";
navbar_link.href = `#${id}`;
const idx = document.createElement("span");
idx.className = "idx";
idx.textContent = String(j).padStart(2, "0") + "\u00A0\u00A0\u00A0";
navbar_link.appendChild(idx);
const text = document.createTextNode(title);
navbar_link.appendChild(text);
navbar_link.dataset.target = id;
navbar_item.appendChild(navbar_link);
create_navbar_entry_mobile(navbar_mobile_links, title, id)
}
function create_navbar_entry_mobile(navbar_mobile_links, title, id) {
const navbar_item = document.createElement("li");
navbar_item.className = "navbar-item-mobile";
navbar_mobile_links.appendChild(navbar_item);
const navbar_link = document.createElement("a");
navbar_link.className = "nav-link-mobile";
navbar_link.href = `#${id}`;
navbar_link.textContent = title;
navbar_item.appendChild(navbar_link);
}
function setCookie(name, value, maxAge) {
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`;
}
function getCookie(name) {
const cookies = document.cookie.split("; ");
for (let c of cookies) {
const [key, value] = c.split("=");
if (key === name) return value;
}
return null;
}
+31
View File
@@ -0,0 +1,31 @@
import { el } from "../core/dom.js";
// Shared skeleton of work and project sections: <section> + header with title,
// plus the matching navbar entry.
export function createSection(container, navbar, item, index) {
const section = el("section", "project-section");
section.id = item.id;
section.setAttribute("data-nav", item.id);
container.appendChild(section);
navbar.addEntry(item.title, item.id, index + 1);
const header = el("div", "section-header");
section.appendChild(header);
header.appendChild(el("h1", "section-title", item.title));
return { section, header };
}
export function appendDescription(header, text) {
header.appendChild(el("p", "section-desc", text));
}
export function appendTags(header, tags) {
const tagList = el("div", "tags");
header.appendChild(tagList);
tags.forEach(tag => {
tagList.appendChild(el("span", "tag", tag));
});
}
+16
View File
@@ -0,0 +1,16 @@
import { el } from "../core/dom.js";
export function renderIntroduction(data, navbar) {
const introduction = document.getElementById("introduction");
introduction.appendChild(el("h1", "", data.title_introduction));
introduction.appendChild(el("p", "", data.content_introduction));
const githubLink = el("a", "text-link introduction", "GitHub ↗");
githubLink.href = "https://github.com/marvinkrausser";
githubLink.target = "_blank";
githubLink.rel = "noopener noreferrer";
introduction.appendChild(githubLink);
navbar.addEntry(data.title_introduction, "introduction", 0);
}
+92
View File
@@ -0,0 +1,92 @@
import { el } from "../core/dom.js";
import { createSection, appendDescription, appendTags } from "./common.js";
export function renderProjects(container, navbar, data, imageConfig) {
navbar.addGroup(data.navbar_group_projects);
data.content_projects.forEach((item, index) => {
const { section, header } = createSection(container, navbar, item, index);
appendDescription(header, item.description);
appendTags(header, item.tags);
const blocks = el("div", "content-blocks");
section.appendChild(blocks);
item.blocks.forEach((block, i) => {
blocks.appendChild(createBlock(block, i, imageConfig));
});
if (item.github) {
section.appendChild(createGithubLink(item.github, data.github_desc));
}
});
}
// One image + text block. Odd blocks put the image on the right.
function createBlock(block, index, imageConfig) {
const contentBlock = el("div", "content-block");
const blockImage = createImage(block.image, imageConfig);
const blockText = createText(block);
if (index % 2 == 1) {
blockImage.className = "block-image right";
blockText.className = "block-text left";
contentBlock.appendChild(blockText);
contentBlock.appendChild(blockImage);
} else {
blockImage.className = "block-image left";
blockText.className = "block-text right";
contentBlock.appendChild(blockImage);
contentBlock.appendChild(blockText);
}
return contentBlock;
}
function createImage(imageId, imageConfig) {
const blockImage = el("div");
const imageObject = imageConfig.images.find(img => img.id === imageId);
const image = el("img", imageObject.image_pos);
image.src = imageObject.image;
image.loading = "lazy";
blockImage.appendChild(image);
return blockImage;
}
function createText(block) {
const blockText = el("div");
blockText.appendChild(el("h2", "", block.heading));
// "--link--" in the text marks where the next entry of block.links goes.
const parts = block.text.split("--link--");
const textContent = el("div", "text-content");
blockText.appendChild(textContent);
parts.forEach((part, i) => {
textContent.appendChild(el("span", "", part));
if (i < parts.length - 1) {
const link = el("a", "text-link", block.links[i].desc);
link.href = block.links[i].link;
link.target = "_blank";
link.rel = "noopener noreferrer";
textContent.appendChild(link);
}
});
return blockText;
}
function createGithubLink(url, text) {
const row = el("div", "link-row");
const link = el("a", "text-link", text);
link.href = url;
link.target = "_blank";
link.rel = "noopener noreferrer";
row.appendChild(link);
return row;
}
+10
View File
@@ -0,0 +1,10 @@
import { el } from "../core/dom.js";
// Status / profession / workplace lines below the name.
export function renderSidebarTop(data) {
const content = document.getElementById("sidebar-top-content");
content.appendChild(el("p", "", data.status));
content.appendChild(el("p", "", data.profession));
content.appendChild(el("p", "", data.workplace));
}
+30
View File
@@ -0,0 +1,30 @@
import { el } from "../core/dom.js";
import { formatDateRange } from "../core/dates.js";
import { createSection, appendDescription, appendTags } from "./common.js";
export function renderWork(container, navbar, data) {
navbar.addGroup(data.navbar_group_work);
data.content_work.forEach((item, index) => {
const { section, header } = createSection(container, navbar, item, index);
appendDescription(header, item.description);
appendDescription(header, formatDateRange(item) + "\n" + item.at + "\n" + item.company);
appendTags(header, item.tech);
section.appendChild(createTasks(item.tasks));
});
}
function createTasks(tasks) {
const tasksDiv = el("div", "tasks-div");
tasks.forEach(task => {
const taskDiv = el("div", "task-div");
taskDiv.appendChild(el("span", "task-title", task.title + ": "));
taskDiv.appendChild(el("span", "task-content", task.content));
tasksDiv.appendChild(taskDiv);
});
return tasksDiv;
}
+61
View File
@@ -0,0 +1,61 @@
import { getCookie, setCookie } from "../core/cookies.js";
const SUPPORTED_LANGUAGES = ["en", "de"];
// Language from cookie, else browser language, else "en".
export function detectLanguage() {
let lang = getCookie("lang");
if (lang == null) {
lang = navigator.language.split("-")[0];
}
if (!SUPPORTED_LANGUAGES.includes(lang)) {
lang = "en";
}
document.documentElement.lang = lang;
return lang;
}
export function setupLanguageDropdown(lang, buttonId, menuId, dropdownId) {
const button = document.getElementById(buttonId);
const menu = document.getElementById(menuId);
const dropdown = document.getElementById(dropdownId);
const items = menu.querySelectorAll(".lang-item");
button.textContent = lang;
button.addEventListener("click", (e) => {
e.stopPropagation();
items.forEach(item => {
item.classList.toggle("hidden", item.dataset.lang == lang);
});
menu.classList.toggle("hidden");
dropdown.classList.toggle("show-border");
});
items.forEach(item => {
item.addEventListener("click", () => {
const lang_selected = item.dataset.lang;
button.textContent = item.textContent;
menu.classList.add("hidden");
dropdown.classList.remove("show-border");
items.forEach(el => el.classList.add("hidden"));
setCookie("lang", lang_selected, "3600");
location.reload();
});
});
document.addEventListener("click", (e) => {
if (!e.target.closest(`#${dropdownId}`)) {
menu.classList.add("hidden");
dropdown.classList.remove("show-border");
items.forEach(el => el.classList.add("hidden"));
}
});
}
+73
View File
@@ -0,0 +1,73 @@
import { el } from "../core/dom.js";
const navbarMobile = () => document.getElementById("navbar-mobile");
const openButton = () => document.getElementById("button-mobile");
// Mobile bar: open/close buttons and closing on outside click.
export function setupMobileNavbar() {
const navbar = navbarMobile();
const button = openButton();
const links = document.getElementById("navbar-mobile-links");
const toggle = () => {
navbar.classList.toggle("inactive");
button.classList.toggle("inactive");
};
document.addEventListener("click", (e) => {
if (!e.target.closest("#navbar-mobile") && !e.target.closest("#button-mobile") && !navbar.classList.contains("inactive")) {
navbar.classList.add("inactive");
button.classList.remove("inactive");
}
});
const closeButton = el("button", "button-mobile navbar", "");
links.appendChild(closeButton);
closeButton.addEventListener("click", toggle);
button.textContent = "";
button.addEventListener("click", toggle);
}
export function hideMobileNavbar() {
navbarMobile().classList.add("inactive");
}
// Desktop sidebar navigation and mobile bar links, filled together.
export function createNavbar() {
const desktop = document.getElementById("navbar");
const mobile = document.getElementById("navbar-mobile-links");
return {
addGroup(title) {
desktop.appendChild(el("li", "navbar-group", title));
},
addEntry(title, id, index) {
addDesktopEntry(desktop, title, id, index);
addMobileEntry(mobile, title, id);
}
};
}
function addDesktopEntry(navbar, title, id, index) {
const item = el("li", "navbar-item");
navbar.appendChild(item);
const link = el("a", "nav-link");
link.href = `#${id}`;
link.dataset.target = id;
item.appendChild(link);
link.appendChild(el("span", "idx", String(index).padStart(2, "0") + "   "));
link.appendChild(document.createTextNode(title));
}
function addMobileEntry(navbar, title, id) {
const item = el("li", "navbar-item-mobile");
navbar.appendChild(item);
const link = el("a", "nav-link-mobile", title);
link.href = `#${id}`;
item.appendChild(link);
}
+48
View File
@@ -0,0 +1,48 @@
// Scroll position restore across reloads (e.g. after switching language).
function saveScroll() {
sessionStorage.setItem("scrollY", window.scrollY);
}
export function setupScrollRestoration() {
history.scrollRestoration = "manual";
window.addEventListener("beforeunload", saveScroll);
}
export function restoreScroll() {
const scrollY = sessionStorage.getItem("scrollY");
if (scrollY !== null) {
document.documentElement.style.scrollBehavior = "auto";
window.scrollTo(0, parseInt(scrollY));
document.documentElement.style.scrollBehavior = "smooth";
}
}
// Highlights the sidebar link of the section currently in view.
export function setupScrollSpy() {
const sections = document.querySelectorAll("[data-nav]");
const navLinks = document.querySelectorAll(".nav-link");
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
navLinks.forEach(link => {
link.classList.toggle(
"active",
link.dataset.target === entry.target.id
);
});
}
});
}, {
threshold: 0,
rootMargin: "-10% 0px -80% 0px"
});
sections.forEach(section => observer.observe(section));
}
+22
View File
@@ -0,0 +1,22 @@
import { getCookie, setCookie } from "../core/cookies.js";
const THEME_MAX_AGE = 60 * 60 * 24 * 7;
// Wires a dark-mode toggle and keeps the other toggles (sidebar / mobile bar) in sync.
export function setupThemeToggle(toggleId, otherToggleIds) {
const toggle = document.getElementById(toggleId);
const otherToggles = otherToggleIds.map(id => document.getElementById(id));
toggle.addEventListener("change", () => {
document.documentElement.classList.toggle("dark", toggle.checked);
setCookie("theme", toggle.checked ? "dark" : "light", THEME_MAX_AGE);
otherToggles.forEach(other => {
other.checked = toggle.checked;
});
});
if (getCookie("theme") === "dark") {
document.documentElement.classList.add("dark");
toggle.checked = true;
}
}
+34
View File
@@ -0,0 +1,34 @@
html {
scroll-behavior: smooth;
}
body {
background: linear-gradient(135deg, var(--bg), var(--bg2));
margin: 0;
}
p,
span,
li,
a {
color: var(--muted);
line-height: 1.7;
font-size: var(--font-size);
font-family: var(--sans);
}
h1,
h2,
h3 {
color: var(--text);
font-family: var(--serif);
}
a {
text-decoration: none;
}
* {
box-sizing: border-box;
}
+3
View File
@@ -0,0 +1,3 @@
.hidden {
display: none;
}
+22
View File
@@ -0,0 +1,22 @@
:root {
--bg: #5E4C93;
--bg2: #1b162a;
--text: #FFFFFF;
--surface: #3e3261;
--muted: #C5C1D1;
--border: #20192a;
--serif: Georgia, serif;
--sans: system-ui, sans-serif;
--nav-w: 240px;
--nav-mobile-w: min(60vw, 400px);
--font-size: 1rem;
}
.dark {
--bg: #130e21;
--bg2: #090710;
--text: #FFFFFF;
--surface: #3e3261;
--muted: #C5C1D1;
--border: #726684;
}
+73
View File
@@ -0,0 +1,73 @@
/* Language Dropdown*/
.lang-dropdown {
user-select: none;
display: flex;
gap: 3px;
width: 91px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border: none;
background: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.lang-dropdown.show-border {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg2);
}
.lang-menu {
display: flex;
align-items: center;
gap: 3px;
}
.lang-item {
cursor: pointer;
background-color: var(--surface);
width: 40px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--muted);
}
@media (hover: hover) and (pointer: fine) {
.lang-item:hover {
border-color: var(--text);
color: var(--text);
}
}
.lang-button {
width: 40px;
height: 30px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
background-color: var(--surface);
color: var(--muted);
border-radius: 8px;
border: 1px solid var(--border);
padding: 3px 6px;
font-size: 14px;
}
@media (hover: hover) and (pointer: fine) {
.lang-button:hover {
border-color: var(--text);
color: var(--text);
}
}
+72
View File
@@ -0,0 +1,72 @@
.button-mobile.no-navbar {
display: inline-block;
}
/* Hidden together with the mobile bar (mobile_bar.css) on large screens */
@media (min-width: 1001px) and (min-height: 701px) {
.button-mobile.no-navbar {
display: none;
}
}
.button-mobile {
position: relative;
width: 40px;
height: 40px;
background: var(--surface);
border: none;
margin-top: 20px;
cursor: pointer;
border: 1px solid var(--border);
}
.button-mobile.navbar {
margin-bottom: 20px;
}
.button-mobile::before {
content: "";
position: absolute;
left: 50%;
top: 30%;
background: var(--muted);
height: 2px;
width: 60%;
transform: translateX(-50%);
}
.button-mobile::after {
content: "";
position: absolute;
left: 50%;
top: 65%;
background: var(--muted);
height: 2px;
width: 60%;
transform: translateX(-50%);
}
.button-mobile:hover::after {
background: var(--text);
}
.button-mobile:hover::before {
background: var(--text);
}
.button-mobile:hover {
width: 40px;
height: 40px;
background: var(--bg2);
}
.button-mobile.no-navbar {
position: fixed;
top: 5px;
left: 5px;
margin-top: 0;
}
.button-mobile.no-navbar.inactive {
display: none;
}
+18
View File
@@ -0,0 +1,18 @@
.tags {
margin-top: 30px;
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag {
color: var(--muted);
background: var(--surface);
border: 1px solid var(--border);
padding: 3px 10px;
border-radius: 3px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
+33
View File
@@ -0,0 +1,33 @@
.text-link:hover {
border-color: var(--text);
}
.text-link {
display: inline-block;
width: fit-content;
font-size: var(--text-size);
font-weight: bolder;
text-decoration: none;
transition: border-color 0.15s;
white-space: nowrap;
position: relative;
}
.text-link:hover {
color: var(--text);
}
.text-link::before {
content: "";
width: 100%;
height: 1.2px;
background: var(--muted);
left: 0;
bottom: 2px;
position: absolute;
border-radius: 16px;
}
.text-link:hover::before {
background: var(--text);
}
+57
View File
@@ -0,0 +1,57 @@
/* Color Switch */
.switch {
user-select: none;
position: relative;
display: inline-block;
width: 52px;
height: 28px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--surface);
border-radius: 26px;
border: 1px solid var(--border);
}
@media (hover: hover) and (pointer: fine) {
.slider:hover {
border: 1px solid var(--text);
}
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: var(--muted);
transition: 0.4s;
border-radius: 50%;
}
input:checked+.slider:before {
background-color: var(--bg);
}
input:checked+.slider {
background-color: var(--surface);
}
input:checked+.slider:before {
transform: translateX(24px);
}
+19
View File
@@ -0,0 +1,19 @@
.main-content {
margin-left: var(--nav-w);
--margin-main-content: 7vw;
--margin-top: 100px;
}
@media (max-width: 1500px) {
.main-content {
--margin-main-content: 4vw;
}
}
@media (max-width: 1000px) or (max-height: 700px) {
.main-content {
margin-left: 0;
--margin-main-content: 20px;
--margin-top: 50px;
}
}
@@ -30,18 +30,11 @@
left: calc(var(--nav-mobile-w) * (-1)) left: calc(var(--nav-mobile-w) * (-1))
} }
.button-mobile.no-navbar { /* Hidden together with the menu button (mobile_menu_button.css) on large screens */
display: inline-block;
}
@media (min-width: 1001px) and (min-height: 701px) { @media (min-width: 1001px) and (min-height: 701px) {
#navbar-mobile { #navbar-mobile {
display: none; display: none;
} }
.button-mobile.no-navbar {
display: none;
}
} }
#navbar-mobile-links { #navbar-mobile-links {
@@ -54,68 +47,6 @@
height: fit-content; height: fit-content;
} }
.button-mobile {
position: relative;
width: 40px;
height: 40px;
background: var(--surface);
border: none;
margin-top: 20px;
cursor: pointer;
border: 1px solid var(--border);
}
.button-mobile.navbar {
margin-bottom: 20px;
}
.button-mobile::before {
content: "";
position: absolute;
left: 50%;
top: 30%;
background: var(--muted);
height: 2px;
width: 60%;
transform: translateX(-50%);
}
.button-mobile::after {
content: "";
position: absolute;
left: 50%;
top: 65%;
background: var(--muted);
height: 2px;
width: 60%;
transform: translateX(-50%);
}
.button-mobile:hover::after {
background: var(--text);
}
.button-mobile:hover::before {
background: var(--text);
}
.button-mobile:hover {
width: 40px;
height: 40px;
background: var(--bg2);
}
.button-mobile.no-navbar {
position: fixed;
top: 5px;
left: 5px;
margin-top: 0;
}
.button-mobile.no-navbar.inactive {
display: none;
}
.navbar-item-mobile { .navbar-item-mobile {
width: 100%; width: 100%;
height: 8vh; height: 8vh;
+62
View File
@@ -0,0 +1,62 @@
/* Navbar */
#navbar {
list-style: none;
display: flex;
flex-direction: column;
padding: var(--padding);
}
.navbar-group {
margin-top: 25px;
margin-bottom: 5px;
font-size: calc(var(--font-size) + 0.1rem);
font-family: var(--serif);
}
.navbar-item {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
font-weight: normal;
margin: 5px 0px 5px 0px;
user-select: none;
}
.nav-link {
font-size: 0.75rem;
}
@media (hover: hover) and (pointer: fine) {
.nav-link:hover {
color: var(--text);
}
}
.nav-link.active {
color: var(--text);
font-weight: bold;
}
.nav-link::before {
content: "";
position: absolute;
background: var(--text);
width: 2px;
height: 0px;
top: 50%;
transform: translateY(-50%);
left: calc(var(--padding) * (-1) + 1px);
border-radius: 20px;
transition: height 0.18s;
}
.nav-link.active::before {
height: 14px;
}
.idx {
font-weight: normal;
font-size: 0.75rem;
}
+89
View File
@@ -0,0 +1,89 @@
.sidebar {
position: fixed;
top: 0;
left: 0;
border-right: 1px solid var(--border);
width: var(--nav-w);
height: 100vh;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
background-color: var(--bg);
--padding: 30px;
}
@media (max-width: 1000px) or (max-height: 700px) {
.sidebar {
display: none;
}
}
/* Sidebar Top */
#sidebar-top {
border-bottom: 1px solid var(--border);
padding: var(--padding);
padding-top: calc(var(--padding) * 2);
display: flex;
flex-direction: column;
gap: 20px;
}
#sidebar-top-head {
padding: 0;
margin: 0;
}
#sidebar-top-content {
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0px;
}
#sidebar-top-content p {
font-size: calc(var(--font-size) - 0.15rem);
margin: 0;
padding: 0;
height: fit-content;
width: fit-content;
}
/* Sidebar Bottom */
.sidebar-bottom-margin {
margin-top: auto;
}
/* Language and Color */
.website-config {
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid var(--border);
padding-bottom: 20px;
gap: 20px;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Contact */
#sidebar-bottom {
height: 100px;
padding: calc(var(--padding) / 2);
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: flex-start;
}
#sidebar-bottom a {
font-size: calc(var(--font-size) - 0.15rem);
}
+27
View File
@@ -0,0 +1,27 @@
/* Entry point. Order matters: later rules win at equal specificity. */
/* Base */
@import "base/variables.css";
@import "base/base.css";
/* Layout */
@import "layout/sidebar.css";
@import "layout/navbar.css";
@import "layout/mobile_bar.css";
@import "layout/main_content.css";
/* Components */
@import "components/theme_switch.css";
@import "components/lang_dropdown.css";
@import "components/mobile_menu_button.css";
@import "components/tags.css";
@import "components/text_link.css";
/* Sections */
@import "sections/introduction.css";
@import "sections/section_header.css";
@import "sections/project_blocks.css";
@import "sections/work.css";
/* Utilities */
@import "base/utilities.css";
-57
View File
@@ -1,57 +0,0 @@
:root {
--bg: #5E4C93;
--bg2: #1b162a;
--text: #FFFFFF;
--surface: #3e3261;
--muted: #C5C1D1;
--border: #20192a;
--serif: Georgia, serif;
--sans: system-ui, sans-serif;
--nav-w: 230px;
--nav-mobile-w: min(60vw, 400px);
--font-size: 1rem;
}
.dark {
--bg: #130e21;
--bg2: #090710;
--text: #FFFFFF;
--surface: #3e3261;
--muted: #C5C1D1;
--border: #726684;
}
html {
scroll-behavior: smooth;
}
body {
background: linear-gradient(135deg, var(--bg), var(--bg2));
margin: 0;
}
p,
span,
li,
a {
color: var(--muted);
line-height: 1.7;
font-size: var(--font-size);
font-family: var(--sans);
}
h1,
h2,
h3 {
color: var(--text);
font-family: var(--serif);
}
a {
text-decoration: none;
}
* {
box-sizing: border-box;
}
-168
View File
@@ -1,168 +0,0 @@
.main-content {
margin-left: var(--nav-w);
--margin-main-content: 7vw;
--margin-top: 100px;
}
@media (max-width: 1500px) {
.main-content {
--margin-main-content: 4vw;
}
}
@media (max-width: 1000px) or (max-height: 700px) {
.main-content {
margin-left: 0;
--margin-main-content: 20px;
--margin-top: 50px;
}
}
#introduction {
margin-top: var(--margin-top);
margin-left: var(--margin-main-content);
margin-bottom: 60px;
margin-right: var(--margin-main-content);
}
#introduction p {
color: var(--text);
}
.text-link.introduction {
margin-top: 20px;
}
.project-section {
border-top: 1px solid var(--border);
}
.content-block {
height: 600px;
display: grid;
grid-template-columns: 1fr 1fr;
overflow: hidden;
margin-top: -1px;
}
.section-header {
margin: 80px;
margin-left: var(--margin-main-content);
margin-right: var(--margin-main-content);
}
.tags {
margin-top: 30px;
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag {
color: var(--muted);
background: var(--surface);
border: 1px solid var(--border);
padding: 3px 10px;
border-radius: 3px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.block-image {
overflow: hidden;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
display: flex;
}
.block-image.right {
margin-right: var(--margin-main-content);
border-left: 1px solid var(--border);
justify-content: left;
margin-left: -0.5px;
}
.block-image.left {
margin-left: var(--margin-main-content);
border-right: 1px solid var(--border);
justify-content: right;
margin-right: -0.5px;
}
.block-image img {
height: 100%;
width: 100%;
max-width: 700px;
max-height: 700px;
object-fit: cover;
}
.block-image img.left {
object-position: left;
}
.block-image img.center {
object-position: center;
}
.block-image img.right {
object-position: right;
}
.block-text {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
margin-right: var(--margin-main-content);
padding-left: var(--margin-main-content);
}
.text-content {
display: inline;
}
.text-link:hover {
border-color: var(--text);
}
.text-link {
display: inline-block;
width: fit-content;
font-size: var(--text-size);
font-weight: bolder;
text-decoration: none;
transition: border-color 0.15s;
white-space: nowrap;
position: relative;
}
.text-link:hover {
color: var(--text);
}
.text-link::before {
content: "";
width: 100%;
height: 1.2px;
background: var(--muted);
left: 0;
bottom: 2px;
position: absolute;
border-radius: 16px;
}
.text-link:hover::before {
background: var(--text);
}
.link-row {
margin: var(--margin-main-content);
margin-top: 30px;
}
.hidden {
display: none;
}
+14
View File
@@ -0,0 +1,14 @@
#introduction {
margin-top: var(--margin-top);
margin-left: var(--margin-main-content);
margin-bottom: 60px;
margin-right: var(--margin-main-content);
}
#introduction p {
color: var(--text);
}
.text-link.introduction {
margin-top: 20px;
}
+66
View File
@@ -0,0 +1,66 @@
.content-block {
height: 600px;
display: grid;
grid-template-columns: 1fr 1fr;
overflow: hidden;
margin-top: -1px;
}
.block-image {
overflow: hidden;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
display: flex;
}
.block-image.right {
margin-right: var(--margin-main-content);
border-left: 1px solid var(--border);
justify-content: left;
margin-left: -0.5px;
}
.block-image.left {
margin-left: var(--margin-main-content);
border-right: 1px solid var(--border);
justify-content: right;
margin-right: -0.5px;
}
.block-image img {
height: 100%;
width: 100%;
max-width: 700px;
max-height: 700px;
object-fit: cover;
}
.block-image img.left {
object-position: left;
}
.block-image img.center {
object-position: center;
}
.block-image img.right {
object-position: right;
}
.block-text {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
margin-right: var(--margin-main-content);
padding-left: var(--margin-main-content);
}
.text-content {
display: inline;
}
.link-row {
margin: var(--margin-main-content);
margin-top: 30px;
}
+9
View File
@@ -0,0 +1,9 @@
.project-section {
border-top: 1px solid var(--border);
}
.section-header {
margin: 80px;
margin-left: var(--margin-main-content);
margin-right: var(--margin-main-content);
}
+60
View File
@@ -0,0 +1,60 @@
/* Work / career sections.
DOM (see scripts/sections/work.js):
.section-header > h1 + p.section-desc (role) + p.section-desc (dates, company) + .tags
.tasks-div > .task-div > span.task-title + span.task-content */
/* Header: tighter than the project headers, since the tasks follow directly */
.section-header:has(+ .tasks-div) {
margin-bottom: 40px;
}
.section-header:has(+ .tasks-div) .section-desc {
margin: 0;
}
.section-header:has(+ .tasks-div) .section-desc:first-of-type {
margin-top: 12px;
color: var(--text);
font-size: calc(var(--font-size) + 0.1rem);
}
.section-header:has(+ .tasks-div) .section-desc + .section-desc {
margin-top: 4px;
font-size: calc(var(--font-size) - 0.05rem);
}
.section-header:has(+ .tasks-div) .tags {
margin-top: 24px;
}
/* Tasks */
.tasks-div {
display: flex;
flex-direction: column;
gap: 28px;
max-width: 75ch;
margin: 0 var(--margin-main-content) 100px;
}
.task-div {
padding-left: 20px;
border-left: 2px solid var(--border);
transition: border-color 0.18s;
}
@media (hover: hover) and (pointer: fine) {
.task-div:hover {
border-color: var(--text);
}
}
.task-title {
color: var(--text);
font-weight: 700;
}
.task-content {
color: var(--muted);
}
-284
View File
@@ -1,284 +0,0 @@
.sidebar {
position: fixed;
top: 0;
left: 0;
border-right: 1px solid var(--border);
width: var(--nav-w);
height: 100vh;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
background-color: var(--bg);
--padding: 30px;
}
@media (max-width: 1000px) or (max-height: 700px) {
.sidebar {
display: none;
}
}
/* Sidebar Top */
#sidebar-top {
border-bottom: 1px solid var(--border);
padding: var(--padding);
padding-top: calc(var(--padding) * 2);
display: flex;
flex-direction: column;
gap: 20px;
}
#sidebar-top-head {
padding: 0;
margin: 0;
}
#sidebar-top-content {
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0px;
}
#sidebar-top-content p {
font-size: calc(var(--font-size) - 0.15rem);
margin: 0;
padding: 0;
height: fit-content;
width: fit-content;
}
/* Navbar */
#navbar {
list-style: none;
display: flex;
flex-direction: column;
padding: var(--padding);
}
.navbar-group {
margin-top: 15px;
margin-bottom: 5px;
font-size: calc(var(--font-size) + 0.1rem);
font-family: var(--serif);
}
.navbar-item {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
font-weight: normal;
margin: 5px 0px 5px 0px;
user-select: none;
}
.nav-link {
font-size: 0.75rem;
}
@media (hover: hover) and (pointer: fine) {
.nav-link:hover {
color: var(--text);
}
}
.nav-link.active {
color: var(--text);
font-weight: bold;
}
.nav-link::before {
content: "";
position: absolute;
background: var(--text);
width: 2px;
height: 0px;
top: 50%;
transform: translateY(-50%);
left: calc(var(--padding) * (-1) + 1px);
border-radius: 20px;
transition: height 0.18s;
}
.nav-link.active::before {
height: 14px;
}
.idx {
font-weight: normal;
font-size: 0.75rem;
}
/* Sidebar Bottom */
.sidebar-bottom-margin {
margin-top: auto;
}
/* Language and Color */
.website-config {
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid var(--border);
padding-bottom: 20px;
gap: 20px;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Color Switch */
.switch {
user-select: none;
position: relative;
display: inline-block;
width: 52px;
height: 28px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--surface);
border-radius: 26px;
border: 1px solid var(--border);
}
@media (hover: hover) and (pointer: fine) {
.slider:hover {
border: 1px solid var(--text);
}
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: var(--muted);
transition: 0.4s;
border-radius: 50%;
}
input:checked+.slider:before {
background-color: var(--bg);
}
input:checked+.slider {
background-color: var(--surface);
}
input:checked+.slider:before {
transform: translateX(24px);
}
/* Language Dropdown*/
.lang-dropdown {
user-select: none;
display: flex;
gap: 3px;
width: 91px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border: none;
background: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.lang-dropdown.show-border {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg2);
}
.lang-menu {
display: flex;
align-items: center;
gap: 3px;
}
.lang-item {
cursor: pointer;
background-color: var(--surface);
width: 40px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--muted);
}
@media (hover: hover) and (pointer: fine) {
.lang-item:hover {
border-color: var(--text);
color: var(--text);
}
}
.lang-button {
width: 40px;
height: 30px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
background-color: var(--surface);
color: var(--muted);
border-radius: 8px;
border: 1px solid var(--border);
padding: 3px 6px;
font-size: 14px;
}
@media (hover: hover) and (pointer: fine) {
.lang-button:hover {
border-color: var(--text);
color: var(--text);
}
}
/* Contact */
#sidebar-bottom {
height: 100px;
padding: calc(var(--padding) / 2);
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: flex-start;
}
#sidebar-bottom a {
font-size: calc(var(--font-size) - 0.15rem);
}