Compare commits
13
Commits
24d84224c4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
498ac6e1a4 | ||
|
|
60f3354e35 | ||
|
|
03e23f9802 | ||
|
|
43203af33e | ||
|
|
e0c424286f | ||
|
|
57ddd4f6a2 | ||
|
|
dfa253bf48 | ||
|
|
e38f26e289 | ||
|
|
cc91f7d4ff | ||
|
|
b09f2029ed | ||
|
|
af21ce0fca | ||
|
|
524c9dc295 | ||
|
|
afafa911d2 |
@@ -0,0 +1,5 @@
|
|||||||
|
.git
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
compose.yaml
|
||||||
|
README.md
|
||||||
+4
-1
@@ -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;"]
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 460 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
+2
-5
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
+21
-1
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -161,4 +92,4 @@
|
|||||||
|
|
||||||
.mobile-config .lang-dropdown {
|
.mobile-config .lang-dropdown {
|
||||||
margin-left: 19.5px;
|
margin-left: 19.5px;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user