Compare commits

..
10 Commits
Author SHA1 Message Date
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
42 changed files with 1375 additions and 960 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
# nginx.conf is needed in the build context but must not be served
RUN rm /usr/share/nginx/html/nginx.conf
EXPOSE 8081
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 -5
View File
@@ -5,10 +5,7 @@
<link rel="icon" href="images/generated_two_no_background.png">
<meta charset="UTF-8">
<title id="website_title"></title>
<link rel="stylesheet" href="styles/main_style.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">
<link rel="stylesheet" href="styles/main.css">
</head>
<body>
@@ -78,7 +75,7 @@
<div id="projects-section"></div>
</div>
<script src="scripts/main_page.js"></script>
<script type="module" src="scripts/main.js"></script>
</body>
+41 -3
View File
@@ -6,8 +6,46 @@
"profession": "Medieninformatik",
"workplace": "Universität Ulm",
"github_desc": "Auf GitHub ansehen ↗",
"navbar_group": "Projekte",
"content": [
"navbar_group_work": "Berufserfahrung",
"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",
"title": "CNN Website",
@@ -28,7 +66,7 @@
"links": [
{
"id": "website",
"link": "https://marvinkrausser.com/bird_cnn",
"link": "https://marvinkrausser.com",
"desc": "Website ↗"
}
]
+41 -3
View File
@@ -6,8 +6,46 @@
"profession": "Media Informatics",
"workplace": "Ulm University",
"github_desc": "View on GitHub ↗",
"navbar_group": "Projects",
"content": [
"navbar_group_work": "Career",
"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",
"title": "CNN Website",
@@ -28,7 +66,7 @@
"links": [
{
"id": "website",
"link": "https://marvinkrausser.com/bird_cnn",
"link": "https://marvinkrausser.com",
"desc": "Website ↗"
}
]
+20
View File
@@ -1,6 +1,26 @@
server {
listen 8081;
server_name localhost;
server_tokens off;
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);
}
+90
View File
@@ -0,0 +1,90 @@
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));
});
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))
}
.button-mobile.no-navbar {
display: inline-block;
}
/* Hidden together with the menu button (mobile_menu_button.css) on large screens */
@media (min-width: 1001px) and (min-height: 701px) {
#navbar-mobile {
display: none;
}
.button-mobile.no-navbar {
display: none;
}
}
#navbar-mobile-links {
@@ -54,68 +47,6 @@
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 {
width: 100%;
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);
}