mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2025-06-29 16:22:01 +02:00
Compare commits
No commits in common. "3acf7d36a4a9a9f4ac9c1291f910ba3a04220f36" and "00e0096f8a22d5021502cf7c332d319354c1f2eb" have entirely different histories.
3acf7d36a4
...
00e0096f8a
158
app/config.yaml
158
app/config.yaml
@ -1,172 +1,137 @@
|
|||||||
---
|
---
|
||||||
accounts:
|
accounts:
|
||||||
name: Online Accounts
|
name: Accounts
|
||||||
description: Discover my online presence.
|
description: My Online Accounts
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-users
|
class: fa-solid fa-users
|
||||||
children:
|
subitems:
|
||||||
- name: Channels
|
- name: Publications
|
||||||
description: Platforms where I share content.
|
description: My Publications
|
||||||
icon:
|
icon:
|
||||||
class: fas fa-newspaper
|
class: fas fa-newspaper
|
||||||
children:
|
subitems:
|
||||||
- name: Microblogs
|
- name: Microblog
|
||||||
description: Stay updated with my microblog posts.
|
description: Read my microblogs
|
||||||
icon:
|
|
||||||
class: fa-solid fa-pen-nib
|
|
||||||
children:
|
|
||||||
- name: Mastodon
|
|
||||||
description: Follow my updates on Mastodon.
|
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-mastodon
|
class: fa-brands fa-mastodon
|
||||||
url: https://microblog.veen.world/@kevinveenbirkenbach
|
url: https://microblog.veen.world/@kevinveenbirkenbach
|
||||||
identifier: "@kevinveenbirkenbach@microblog.veen.world"
|
|
||||||
- name: Twitter
|
|
||||||
description: Follow me on Twitter (limited use).
|
|
||||||
icon:
|
|
||||||
class: fa-brands fa-twitter
|
|
||||||
url: https://s.veen.world/twitter
|
|
||||||
identifier: kevinbirkenbach
|
|
||||||
warning: I rarely use X/Twitter and recommend alternative platforms like Mastodon.
|
|
||||||
alternatives:
|
|
||||||
- link: accounts.channels.microblogs.mastodon
|
|
||||||
- name: Bluesky
|
|
||||||
description: Follow me on Bluesky (coming soon).
|
|
||||||
icon:
|
|
||||||
class: fa-brands fa-bluesky
|
|
||||||
alternatives:
|
|
||||||
- link: accounts.channels.microblogs.mastodon
|
|
||||||
|
|
||||||
- name: Pictures
|
- name: Pictures
|
||||||
description: View my photography.
|
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-images
|
class: fa-solid fa-images
|
||||||
children:
|
subitems:
|
||||||
- name: Pixelfed
|
- name: Pixelfed
|
||||||
description: Explore my photo gallery on Pixelfed.
|
description: View my photo gallery
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-camera
|
class: fa-solid fa-camera
|
||||||
url: https://s.veen.world/pictures
|
url: https://s.veen.world/pictures
|
||||||
- name: Instagram
|
- name: Instagram
|
||||||
description: Follow me on Instagram.
|
description: Follow me on Instagram
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-instagram
|
class: fa-brands fa-instagram
|
||||||
url: https://www.instagram.com/kevinveenbirkenbach/
|
url: https://www.instagram.com/kevinveenbirkenbach/
|
||||||
identifier: kevinveenbirkenbach
|
identifier: kevinveenbirkenbach
|
||||||
warning: Platforms by Meta (e.g., Instagram, Facebook) may compromise your data privacy. Consider using decentralized alternatives.
|
warning: Using software and platforms from the Meta corporation (e.g., Facebook, Instagram, WhatsApp) may compromise your data privacy and digital freedom due to centralized control, extensive data collection practices, and inconsistent moderation policies. These platforms often fail to adequately address harmful content, misinformation, and abuse.
|
||||||
alternatives:
|
|
||||||
- link: accounts.channels.pictures.pixelfed
|
|
||||||
|
|
||||||
- name: Videos
|
- name: Videos
|
||||||
description: Watch my video content.
|
description: Watch my videos
|
||||||
icon:
|
|
||||||
class: fa-solid fa-video
|
|
||||||
children:
|
|
||||||
- name: Peertube
|
|
||||||
description: Discover my videos on Peertube.
|
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-video
|
class: fa-solid fa-video
|
||||||
url: https://s.veen.world/videos
|
url: https://s.veen.world/videos
|
||||||
- name: YouTube
|
|
||||||
description: Follow me on YouTube (inactive).
|
|
||||||
icon:
|
|
||||||
class: fa-brands fa-youtube
|
|
||||||
url: https://s.veen.world/youtube
|
|
||||||
warning: I no longer publish videos on YouTube. Please visit my Peertube channel instead.
|
|
||||||
alternatives:
|
|
||||||
- link: accounts.channels.videos.peertube
|
|
||||||
|
|
||||||
- name: Blog
|
- name: Blog
|
||||||
description: Read my articles and stories.
|
description: Read my blog
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-blog
|
class: fa-solid fa-blog
|
||||||
url: https://blog.veen.world
|
url: https://blog.veen.world
|
||||||
|
|
||||||
- name: Code
|
- name: Code
|
||||||
description: Access my coding projects.
|
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-laptop-code
|
class: fa-solid fa-laptop-code
|
||||||
children:
|
description: Check out my Code
|
||||||
- name: GitHub
|
subitems:
|
||||||
description: View my GitHub repositories.
|
- name: Github
|
||||||
|
description: View my GitHub profile
|
||||||
icon:
|
icon:
|
||||||
class: bi bi-github
|
class: bi bi-github
|
||||||
url: https://github.com/kevinveenbirkenbach
|
url: https://github.com/kevinveenbirkenbach
|
||||||
|
|
||||||
- name: Gitea
|
- name: Gitea
|
||||||
description: Explore my self-hosted repositories.
|
description: Explore my code repositories
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-code
|
class: fa-solid fa-code
|
||||||
url: https://git.veen.world/kevinveenbirkenbach
|
url: https://git.veen.world/kevinveenbirkenbach
|
||||||
|
|
||||||
- name: Social Media
|
- name: Social Media
|
||||||
description: Social and developer platforms.
|
description: Social and developer networks
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-meta
|
class: fa-brands fa-meta
|
||||||
children:
|
url:
|
||||||
|
subitems:
|
||||||
- name: Facebook
|
- name: Facebook
|
||||||
description: Visit my Facebook page.
|
description: Like my Facebook page
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-facebook
|
class: fa-brands fa-facebook
|
||||||
url: https://www.facebook.com/kevinveenbirkenbach
|
url: https://www.facebook.com/kevinveenbirkenbach
|
||||||
|
|
||||||
- link: navigation.header.contact.messenger
|
- link: navigation.header.contact.messenger
|
||||||
|
- name: Carreer Profiles
|
||||||
- name: Career Profiles
|
|
||||||
description: Professional networking profiles.
|
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-user-tie
|
class: fa-solid fa-user-tie
|
||||||
children:
|
subitems:
|
||||||
- name: XING
|
- name: XING
|
||||||
description: View my XING profile.
|
description: Visit my XING profile
|
||||||
icon:
|
icon:
|
||||||
class: bi bi-building
|
class: bi bi-building
|
||||||
url: https://www.xing.com/profile/Kevin_VeenBirkenbach
|
url: https://www.xing.com/profile/Kevin_VeenBirkenbach
|
||||||
|
|
||||||
- name: LinkedIn
|
- name: LinkedIn
|
||||||
description: Connect with me on LinkedIn.
|
description: Connect on LinkedIn
|
||||||
icon:
|
icon:
|
||||||
class: bi bi-linkedin
|
class: bi bi-linkedin
|
||||||
url: https://www.linkedin.com/in/kevinveenbirkenbach
|
url: https://www.linkedin.com/in/kevinveenbirkenbach
|
||||||
|
|
||||||
- name: Sports
|
- name: Sports
|
||||||
description: My sports activities and logs.
|
description: My sport activities
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-running
|
class: fa-solid fa-running
|
||||||
children:
|
url:
|
||||||
|
subitems:
|
||||||
- name: Garmin
|
- name: Garmin
|
||||||
description: Explore my Garmin activity records.
|
description: My Garmin activities
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-person-running
|
class: fa-solid fa-person-running
|
||||||
url: https://s.veen.world/garmin
|
url: https://s.veen.world/garmin
|
||||||
|
|
||||||
- name: Eversports
|
- name: Eversports
|
||||||
description: View my Eversports sessions.
|
description: My Eversports sessions
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-dumbbell
|
class: fa-solid fa-dumbbell
|
||||||
url: https://s.veen.world/eversports
|
url: https://s.veen.world/eversports
|
||||||
|
|
||||||
- name: Duolingo
|
- name: Duolingo
|
||||||
description: Join me in language learning.
|
description: Learn with me on Duolingo
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-language
|
class: fa-solid fa-language
|
||||||
url: https://www.duolingo.com/profile/kevinbirkenbach
|
url: https://www.duolingo.com/profile/kevinbirkenbach
|
||||||
|
|
||||||
- name: Spotify
|
- name: Spotify
|
||||||
description: Listen to my playlists.
|
description: Listen to my playlists
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-spotify
|
class: fa-brands fa-spotify
|
||||||
url: https://open.spotify.com/user/31vebfzbjf3p7oualis76qfpr5ty
|
url: https://open.spotify.com/user/31vebfzbjf3p7oualis76qfpr5ty
|
||||||
|
|
||||||
- name: Patreon
|
- name: Patreon
|
||||||
description: Support my work on Patreon.
|
description: Support me on Patreon
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-patreon
|
class: fa-brands fa-patreon
|
||||||
url: https://patreon.com/kevinveenbirkenbach
|
url: https://patreon.com/kevinveenbirkenbach
|
||||||
|
|
||||||
- name: Discourse
|
- name: Discourse
|
||||||
description: Join discussions on my forum.
|
description: Follow me on Discourse
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-discourse
|
class: fa-brands fa-discourse
|
||||||
url: https://forum.veen.world/u/kevinveenbirkenbach
|
url: https://forum.veen.world/u/kevinveenbirkenbach
|
||||||
|
|
||||||
|
|
||||||
cards:
|
cards:
|
||||||
- icon:
|
- icon:
|
||||||
source: https://cloud.veen.world/s/logo_agile_coach_512x512/download
|
source: https://cloud.veen.world/s/logo_agile_coach_512x512/download
|
||||||
@ -286,18 +251,16 @@ company:
|
|||||||
imprint_url: https://s.veen.world/imprint
|
imprint_url: https://s.veen.world/imprint
|
||||||
navigation:
|
navigation:
|
||||||
header:
|
header:
|
||||||
children:
|
|
||||||
- link: accounts.channels.children
|
|
||||||
- name: Contact
|
- name: Contact
|
||||||
description: Get in touch
|
description: Get in touch
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-envelope
|
class: fa-solid fa-envelope
|
||||||
children:
|
subitems:
|
||||||
- name: Email
|
- name: Email
|
||||||
description: Send me an email
|
description: Send me an email
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-envelope
|
class: fa-solid fa-envelope
|
||||||
children:
|
subitems:
|
||||||
- name: Email
|
- name: Email
|
||||||
description: Send me an email
|
description: Send me an email
|
||||||
icon:
|
icon:
|
||||||
@ -335,7 +298,7 @@ navigation:
|
|||||||
description: Social and developer networks
|
description: Social and developer networks
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-comments
|
class: fa-solid fa-comments
|
||||||
children:
|
subitems:
|
||||||
- name: Matrix
|
- name: Matrix
|
||||||
description: Chat with me on Matrix
|
description: Chat with me on Matrix
|
||||||
icon:
|
icon:
|
||||||
@ -382,20 +345,20 @@ navigation:
|
|||||||
- link: navigation.header.contact.messenger.matrix
|
- link: navigation.header.contact.messenger.matrix
|
||||||
- link: navigation.header.contact.messenger.signal
|
- link: navigation.header.contact.messenger.signal
|
||||||
- link: navigation.header.contact.messenger.telegram
|
- link: navigation.header.contact.messenger.telegram
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
children:
|
|
||||||
- link: accounts
|
- link: accounts
|
||||||
- name: Solution Hub
|
- name: Solution Hub
|
||||||
description: Curated collection of self hosted tools
|
description: Curated collection of self hosted tools
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-network-wired
|
class: fa-solid fa-network-wired
|
||||||
url:
|
url:
|
||||||
children:
|
subitems:
|
||||||
- name: Community
|
- name: Community
|
||||||
description: Tools to manage the community
|
description: Tools to manage the community
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-users
|
class: fa-solid fa-users
|
||||||
children:
|
subitems:
|
||||||
- name: Forum
|
- name: Forum
|
||||||
description: Join the discussion
|
description: Join the discussion
|
||||||
icon:
|
icon:
|
||||||
@ -415,7 +378,7 @@ navigation:
|
|||||||
description: Project Management Tools
|
description: Project Management Tools
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-chart-line
|
class: fa-solid fa-chart-line
|
||||||
children:
|
subitems:
|
||||||
- name: Open Project
|
- name: Open Project
|
||||||
description: Explore my projects
|
description: Explore my projects
|
||||||
icon:
|
icon:
|
||||||
@ -431,7 +394,7 @@ navigation:
|
|||||||
- name: Communication
|
- name: Communication
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-comments
|
class: fa-solid fa-comments
|
||||||
children:
|
subitems:
|
||||||
- name: Elements
|
- name: Elements
|
||||||
description: Chat with me
|
description: Chat with me
|
||||||
icon:
|
icon:
|
||||||
@ -452,7 +415,7 @@ navigation:
|
|||||||
- name: Tools
|
- name: Tools
|
||||||
icon:
|
icon:
|
||||||
class: fas fa-tools
|
class: fas fa-tools
|
||||||
children:
|
subitems:
|
||||||
- name: Matomo
|
- name: Matomo
|
||||||
description: Analyze with Matomo
|
description: Analyze with Matomo
|
||||||
icon:
|
icon:
|
||||||
@ -480,12 +443,12 @@ navigation:
|
|||||||
description: All information about me
|
description: All information about me
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-user
|
class: fa-solid fa-user
|
||||||
children:
|
subitems:
|
||||||
- name: Logbooks
|
- name: Logbooks
|
||||||
description: Access my personal logbooks (diving, flying, sailing)
|
description: Access my personal logbooks (diving, flying, sailing)
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-book
|
class: fa-solid fa-book
|
||||||
children:
|
subitems:
|
||||||
- name: Skydiver
|
- name: Skydiver
|
||||||
description: View my skydiving logs
|
description: View my skydiving logs
|
||||||
icon:
|
icon:
|
||||||
@ -520,21 +483,11 @@ navigation:
|
|||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-file-lines
|
class: fa-solid fa-file-lines
|
||||||
url: https://s.veen.world/lebenslauf
|
url: https://s.veen.world/lebenslauf
|
||||||
- name: Languages
|
|
||||||
icon:
|
|
||||||
class: fa-solid fa-language
|
|
||||||
children:
|
|
||||||
- link: accounts.duolingo
|
|
||||||
- name: Languages Credentials
|
|
||||||
description: Check out which languages I speak
|
|
||||||
url: https://s.veen.world/languages
|
|
||||||
icon:
|
|
||||||
class: fa-solid fa-language
|
|
||||||
- name: Credentials
|
- name: Credentials
|
||||||
description: Access my certifications, degrees, and professional records
|
description: Access my certifications, degrees, and professional records
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-id-card
|
class: fa-solid fa-id-card
|
||||||
children:
|
subitems:
|
||||||
- name: Degrees
|
- name: Degrees
|
||||||
description: View my academic degrees
|
description: View my academic degrees
|
||||||
icon:
|
icon:
|
||||||
@ -550,15 +503,10 @@ navigation:
|
|||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-scroll
|
class: fa-solid fa-scroll
|
||||||
url: https://s.veen.world/certifications
|
url: https://s.veen.world/certifications
|
||||||
- name: Skill Matrix
|
|
||||||
description: Checkout my skills
|
|
||||||
icon:
|
|
||||||
class: fa-solid fa-layer-group
|
|
||||||
url: https://s.veen.world/skillmatrix
|
|
||||||
- link: accounts
|
- link: accounts
|
||||||
|
|
||||||
- name: Imprint
|
- name: Imprint
|
||||||
description: Check out the imprint information
|
description: Check out the imprint information
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-scale-balanced
|
class: fa-solid fa-scale-balanced
|
||||||
url: https://s.veen.world/imprint
|
url: https://s.veen.world/imprint
|
||||||
|
|
@ -23,17 +23,13 @@ a {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle shadow effect */
|
|
||||||
.navbar, .card, .dropdown-menu{
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card styles */
|
/* Card styles */
|
||||||
.navbar, .card {
|
.navbar, .card {
|
||||||
flex: 1; /* Ensures cards fill the height of their container */
|
flex: 1; /* Ensures cards fill the height of their container */
|
||||||
border-radius: 5px; /* Rounded corners */
|
border-radius: 5px; /* Rounded corners */
|
||||||
border: 1px solid #ccc; /* Optional border color */
|
border: 1px solid #ccc; /* Optional border color */
|
||||||
padding: 10px; /* Inner spacing */
|
padding: 10px; /* Inner spacing */
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); /* Subtle shadow effect */
|
||||||
color: #000000 !important;
|
color: #000000 !important;
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,34 @@
|
|||||||
display: none;
|
display: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
width: max-content !important; /* Passt die Breite an das breiteste Item an */
|
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||||
box-sizing: border-box; /* Berücksichtigt Innenabstand und Rahmen */
|
}
|
||||||
|
|
||||||
|
/* Dropdown-Menü beim Hover anzeigen */
|
||||||
|
.nav-item.dropdown:hover > .dropdown-menu,
|
||||||
|
.dropdown-submenu:hover > .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown-Menü bei der Klasse "open" anzeigen */
|
||||||
|
.nav-item.dropdown.open > .dropdown-menu,
|
||||||
|
.dropdown-submenu.open > .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Positionierung von Submenüs */
|
/* Positionierung von Submenüs */
|
||||||
.dropdown-submenu > .dropdown-menu {
|
.dropdown-submenu > .dropdown-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
|
left: 100%; /* Rechts ausklappen */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu.open > .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
}
|
}
|
@ -2,47 +2,57 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const menuItems = document.querySelectorAll('.nav-item.dropdown');
|
const menuItems = document.querySelectorAll('.nav-item.dropdown');
|
||||||
const subMenuItems = document.querySelectorAll('.dropdown-submenu');
|
const subMenuItems = document.querySelectorAll('.dropdown-submenu');
|
||||||
|
|
||||||
function addMenuEventListeners(items, isTopLevel) {
|
menuItems.forEach(item => {
|
||||||
items.forEach(item => {
|
|
||||||
let timeout;
|
let timeout;
|
||||||
|
|
||||||
function onMouseEnter() {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
openMenu(item, isTopLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseLeave() {
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
closeMenu(item);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Öffnen beim Hovern
|
// Öffnen beim Hovern
|
||||||
item.addEventListener('mouseenter', onMouseEnter);
|
item.addEventListener('mouseenter', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
openMenu(item, true);
|
||||||
|
});
|
||||||
|
|
||||||
// Verzögertes Schließen beim Verlassen
|
// Verzögertes Schließen beim Verlassen
|
||||||
item.addEventListener('mouseleave', onMouseLeave);
|
item.addEventListener('mouseleave', () => {
|
||||||
|
timeout = setTimeout(() => closeMenu(item), 500);
|
||||||
|
});
|
||||||
|
|
||||||
// Öffnen und Position anpassen beim Klicken
|
// Öffnen und Position anpassen beim Klicken
|
||||||
item.addEventListener('click', (e) => {
|
item.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault(); // Verhindert die Standardaktion
|
||||||
e.stopPropagation(); // Verhindert das Schließen von Menüs bei Klick
|
e.stopPropagation(); // Verhindert das Schließen von Menüs bei Klick
|
||||||
if (item.classList.contains('open')) {
|
if (item.classList.contains('open')) {
|
||||||
closeMenu(item);
|
closeMenu(item);
|
||||||
} else {
|
} else {
|
||||||
openMenu(item, isTopLevel);
|
openMenu(item, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function addAllMenuEventListeners() {
|
subMenuItems.forEach(item => {
|
||||||
const updatedMenuItems = document.querySelectorAll('.nav-item.dropdown');
|
let timeout;
|
||||||
const updatedSubMenuItems = document.querySelectorAll('.dropdown-submenu');
|
|
||||||
addMenuEventListeners(updatedMenuItems, true);
|
|
||||||
addMenuEventListeners(updatedSubMenuItems, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
addAllMenuEventListeners();
|
// Öffnen beim Hovern
|
||||||
|
item.addEventListener('mouseenter', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
openMenu(item, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verzögertes Schließen beim Verlassen
|
||||||
|
item.addEventListener('mouseleave', () => {
|
||||||
|
timeout = setTimeout(() => closeMenu(item), 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Öffnen und Position anpassen beim Klicken
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault(); // Verhindert die Standardaktion
|
||||||
|
e.stopPropagation(); // Verhindert das Schließen von Menüs bei Klick
|
||||||
|
if (item.classList.contains('open')) {
|
||||||
|
closeMenu(item);
|
||||||
|
} else {
|
||||||
|
openMenu(item, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Globale Klick-Listener, um Menüs zu schließen, wenn außerhalb geklickt wird
|
// Globale Klick-Listener, um Menüs zu schließen, wenn außerhalb geklickt wird
|
||||||
document.addEventListener('click', () => {
|
document.addEventListener('click', () => {
|
||||||
@ -53,10 +63,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
item.classList.add('open');
|
item.classList.add('open');
|
||||||
const submenu = item.querySelector('.dropdown-menu');
|
const submenu = item.querySelector('.dropdown-menu');
|
||||||
if (submenu) {
|
if (submenu) {
|
||||||
|
adjustMenuPosition(submenu, item, isTopLevel);
|
||||||
submenu.style.display = 'block';
|
submenu.style.display = 'block';
|
||||||
submenu.style.opacity = '1';
|
submenu.style.opacity = '1';
|
||||||
submenu.style.visibility = 'visible';
|
submenu.style.visibility = 'visible';
|
||||||
adjustMenuPosition(submenu, item, isTopLevel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +99,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (isTopLevel) {
|
if (isTopLevel) {
|
||||||
// Top-Level-Menüs öffnen nur nach oben oder unten
|
// Top-Level-Menüs öffnen nur nach oben oder unten
|
||||||
if (spaceBelow < rect.height && spaceAbove > rect.height) {
|
if (spaceBelow < rect.height && spaceAbove > rect.height) {
|
||||||
submenu.style.bottom = `${window.innerHeight - parentRect.bottom - parentRect.height}px`;
|
submenu.style.bottom = '100%';
|
||||||
submenu.style.top = 'auto';
|
submenu.style.top = 'auto';
|
||||||
} else {
|
} else {
|
||||||
submenu.style.top = `${parentRect.height}px`;
|
submenu.style.top = `${parentRect.height}px`;
|
||||||
@ -101,14 +111,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
submenu.style.left = prefersRight ? '100%' : 'auto';
|
submenu.style.left = prefersRight ? '100%' : 'auto';
|
||||||
submenu.style.right = prefersRight ? 'auto' : '100%';
|
submenu.style.right = prefersRight ? 'auto' : '100%';
|
||||||
|
|
||||||
// Öffnen nach oben, wenn unten kein Platz ist
|
const prefersBelow = spaceBelow >= spaceAbove;
|
||||||
if (spaceBelow < rect.height && spaceAbove > rect.height) {
|
submenu.style.top = prefersBelow ? '0' : 'auto';
|
||||||
submenu.style.top = 'auto';
|
submenu.style.bottom = prefersBelow ? 'auto' : '100%';
|
||||||
submenu.style.bottom = `${parentRect.bottom - parentRect.top - rect.height}px`; // Höhe des Submenüs wird berücksichtigt
|
|
||||||
} else {
|
|
||||||
submenu.style.top = '0';
|
|
||||||
submenu.style.bottom = 'auto';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,33 +1,33 @@
|
|||||||
{% macro render_icon_and_name(item) %}
|
<!-- Template for Subitems -->
|
||||||
<i class="{{ item.icon.class if item.icon is defined and item.icon.class is defined else 'fa-solid fa-link' }}"></i>
|
{% macro render_subitems(subitems) %}
|
||||||
{% if item.name is defined %}
|
{% for subitem in subitems %}
|
||||||
{{ item.name }}
|
{% if subitem.subitems %}
|
||||||
{% else %}
|
|
||||||
Unnamed Item: {{item}}
|
|
||||||
{% endif %}
|
|
||||||
{% endmacro %}
|
|
||||||
<!-- Template for children -->
|
|
||||||
{% macro render_children(children) %}
|
|
||||||
{% for children in children %}
|
|
||||||
{% if children.children %}
|
|
||||||
<li class="dropdown-submenu position-relative">
|
<li class="dropdown-submenu position-relative">
|
||||||
<a class="dropdown-item dropdown-toggle" title="{{ children.description }}">
|
<a class="dropdown-item dropdown-toggle" href="#" title="{{ subitem.description }}">
|
||||||
{{ render_icon_and_name(children) }}
|
{% if subitem.icon is defined and subitem.icon.class is defined %}
|
||||||
|
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
|
||||||
|
{% else %}
|
||||||
|
<p>Missing icon in subitem: {{ subitem }}</p>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{{ render_children(children.children) }}
|
{{ render_subitems(subitem.subitems) }}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% elif children.identifier or children.warning or children.info %}
|
{% elif subitem.identifier or subitem.warning or subitem.info %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" onclick='openDynamicPopup({{ children|tojson|safe }})' data-bs-toggle="tooltip" title="{{ children.description }}">
|
<a class="dropdown-item" onclick='openDynamicPopup({{ subitem|tojson|safe }})' data-bs-toggle="tooltip" title="{{ subitem.description }}">
|
||||||
{{ render_icon_and_name(children) }}
|
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{{ children.url }}" target="{{ children.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ children.description }}">
|
<a class="dropdown-item" href="{{ subitem.url }}" target="{{ subitem.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ subitem.description }}">
|
||||||
{{ render_icon_and_name(children) }}
|
{% if subitem.icon is defined and subitem.icon.class is defined %}
|
||||||
|
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
|
||||||
|
{% else %}
|
||||||
|
<p>Missing icon in subitem: {{ subitem }}</p>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -42,26 +42,26 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
|
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
|
||||||
<ul class="navbar-nav {% if menu_type == 'header' %}ms-auto{% endif %}">
|
<ul class="navbar-nav {% if menu_type == 'header' %}ms-auto{% endif %}">
|
||||||
{% for item in navigation[menu_type].children %}
|
{% for item in navigation[menu_type] %}
|
||||||
{% if item.url %}
|
{% if item.url %}
|
||||||
<!-- Single Item -->
|
<!-- Single Item -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ item.url }}" target="{{ item.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ item.description }}">
|
<a class="nav-link" href="{{ item.url }}" target="{{ item.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ item.description }}">
|
||||||
{{ render_icon_and_name(item) }}
|
<i class="{{ item.icon.class }}"></i> {{ item.name }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Dropdown Menu -->
|
<!-- Dropdown Menu -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" id="navbarDropdown{{ loop.index }}" role="button" data-bs-toggle="dropdown" data-bs-display="dynamic" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown{{ loop.index }}" role="button" data-bs-toggle="dropdown" data-bs-display="dynamic" aria-expanded="false">
|
||||||
{% if item.icon is defined and item.icon.class is defined %}
|
{% if item.icon is defined and item.icon.class is defined %}
|
||||||
{{ render_icon_and_name(item) }}
|
<i class="{{ item.icon.class }}"></i> {{ item.name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Missing icon in item: {{ item }}</p>
|
<p>Missing icon in item: {{ item }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{{ render_children(item.children) }}
|
{{ render_subitems(item.subitems) }}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -2,7 +2,7 @@ from pprint import pprint
|
|||||||
class ConfigurationResolver:
|
class ConfigurationResolver:
|
||||||
"""
|
"""
|
||||||
A class to resolve `link` entries in a nested configuration structure.
|
A class to resolve `link` entries in a nested configuration structure.
|
||||||
Supports navigation through dictionaries, lists, and `children`.
|
Supports navigation through dictionaries, lists, and `subitems`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
@ -14,56 +14,23 @@ class ConfigurationResolver:
|
|||||||
"""
|
"""
|
||||||
self._recursive_resolve(self.config, self.config)
|
self._recursive_resolve(self.config, self.config)
|
||||||
|
|
||||||
def __load_children(self,path):
|
|
||||||
"""
|
|
||||||
Check if explicitly children should be loaded and not parent
|
|
||||||
"""
|
|
||||||
return path.split('.').pop() == "children"
|
|
||||||
|
|
||||||
def _replace_in_dict_by_dict(self, dict_origine, old_key, new_dict):
|
|
||||||
if old_key in dict_origine:
|
|
||||||
# Entferne den alten Key
|
|
||||||
old_value = dict_origine.pop(old_key)
|
|
||||||
# Füge die neuen Key-Value-Paare hinzu
|
|
||||||
dict_origine.update(new_dict)
|
|
||||||
|
|
||||||
def _replace_in_list_by_list(self, list_origine, old_element, new_elements):
|
|
||||||
index = list_origine.index(old_element)
|
|
||||||
list_origine[index:index+1] = new_elements
|
|
||||||
|
|
||||||
def _replace_element_in_list(self, list_origine, old_element, new_element):
|
|
||||||
index = list_origine.index(old_element)
|
|
||||||
list_origine[index] = new_element
|
|
||||||
|
|
||||||
def _recursive_resolve(self, current_config, root_config):
|
def _recursive_resolve(self, current_config, root_config):
|
||||||
"""
|
"""
|
||||||
Recursively resolves `link` entries in the configuration.
|
Recursively resolves `link` entries in the configuration.
|
||||||
"""
|
"""
|
||||||
if isinstance(current_config, dict):
|
if isinstance(current_config, dict):
|
||||||
for key, value in list(current_config.items()):
|
for key, value in list(current_config.items()):
|
||||||
if key == "children":
|
if key == "link":
|
||||||
if value is None or not isinstance(value, list):
|
|
||||||
raise ValueError(f"Expected 'children' to be a list, but got {type(value).__name__} instead.")
|
|
||||||
for item in value:
|
|
||||||
if "link" in item:
|
|
||||||
loaded_link = self._find_entry(root_config, item['link'].lower(), False)
|
|
||||||
if isinstance(loaded_link, list):
|
|
||||||
self._replace_in_list_by_list(value,item,loaded_link)
|
|
||||||
else:
|
|
||||||
self._replace_element_in_list(value,item,loaded_link)
|
|
||||||
else:
|
|
||||||
self._recursive_resolve(value, root_config)
|
|
||||||
elif key == "link":
|
|
||||||
try:
|
try:
|
||||||
loaded = self._find_entry(root_config, value.lower(), True)
|
target = self._find_entry(root_config, value.lower(), True)
|
||||||
if isinstance(loaded, list) and len(loaded) > 2:
|
if isinstance(target, list) and len(target) > 2:
|
||||||
loaded = self._find_entry(root_config, value.lower(), False)
|
target = self._find_entry(root_config, value.lower(), False)
|
||||||
current_config.clear()
|
current_config.clear()
|
||||||
current_config.update(loaded)
|
current_config.update(target)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Error resolving link '{value}': {str(e)}. "
|
f"Error resolving link '{value}': {str(e)}. "
|
||||||
f"Current path: {key}, Current config: {current_config}" + (f", Loaded: {loaded}" if 'loaded' in locals() or 'loaded' in globals() else "")
|
f"Current path: {key}, Current config: {current_config}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._recursive_resolve(value, root_config)
|
self._recursive_resolve(value, root_config)
|
||||||
@ -71,9 +38,9 @@ class ConfigurationResolver:
|
|||||||
for item in current_config:
|
for item in current_config:
|
||||||
self._recursive_resolve(item, root_config)
|
self._recursive_resolve(item, root_config)
|
||||||
|
|
||||||
def _get_children(self,current):
|
def _get_subitems(self,current):
|
||||||
if isinstance(current, dict) and ("children" in current and current["children"]):
|
if isinstance(current, dict) and ("subitems" in current and current["subitems"]):
|
||||||
current = current["children"]
|
current = current["subitems"]
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def _find_by_name(self,current, part):
|
def _find_by_name(self,current, part):
|
||||||
@ -82,21 +49,18 @@ class ConfigurationResolver:
|
|||||||
None
|
None
|
||||||
)
|
)
|
||||||
|
|
||||||
def _find_entry(self, config, path, children):
|
def _find_entry(self, config, path, subitems):
|
||||||
"""
|
"""
|
||||||
Finds an entry in the configuration by a dot-separated path.
|
Finds an entry in the configuration by a dot-separated path.
|
||||||
Supports both dictionaries and lists with `children` navigation.
|
Supports both dictionaries and lists with `subitems` navigation.
|
||||||
"""
|
"""
|
||||||
parts = path.split('.')
|
parts = path.split('.')
|
||||||
current = config
|
current = config
|
||||||
for part in parts:
|
for part in parts:
|
||||||
if isinstance(current, list):
|
if isinstance(current, list):
|
||||||
# If children explicit declared just load children
|
|
||||||
if part != "children":
|
|
||||||
# Look for a matching name in the list
|
# Look for a matching name in the list
|
||||||
found = self._find_by_name(current,part)
|
found = self._find_by_name(current,part)
|
||||||
if found:
|
if found:
|
||||||
current = found
|
|
||||||
print(
|
print(
|
||||||
f"Matching entry for '{part}' in list. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
|
f"Matching entry for '{part}' in list. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
|
||||||
f"Current list: {current}"
|
f"Current list: {current}"
|
||||||
@ -106,11 +70,12 @@ class ConfigurationResolver:
|
|||||||
f"No matching entry for '{part}' in list. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
|
f"No matching entry for '{part}' in list. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
|
||||||
f"Current list: {current}"
|
f"Current list: {current}"
|
||||||
)
|
)
|
||||||
|
current = found
|
||||||
elif isinstance(current, dict):
|
elif isinstance(current, dict):
|
||||||
# Case-insensitive dictionary lookup
|
# Case-insensitive dictionary lookup
|
||||||
key = next((k for k in current if k.lower() == part), None)
|
key = next((k for k in current if k.lower() == part), None)
|
||||||
if key is None:
|
if key is None:
|
||||||
current = self._find_by_name(current["children"],part)
|
current = self._find_by_name(current["subitems"],part)
|
||||||
if not current:
|
if not current:
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
f"Key '{part}' not found in dictionary. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
|
f"Key '{part}' not found in dictionary. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
|
||||||
@ -124,8 +89,8 @@ class ConfigurationResolver:
|
|||||||
f"Invalid path segment '{part}'. Current type: {type(current)}. "
|
f"Invalid path segment '{part}'. Current type: {type(current)}. "
|
||||||
f"Path so far: {' > '.join(parts[:parts.index(part)+1])}"
|
f"Path so far: {' > '.join(parts[:parts.index(part)+1])}"
|
||||||
)
|
)
|
||||||
if children:
|
if subitems:
|
||||||
current = self._get_children(current)
|
current = self._get_subitems(current)
|
||||||
|
|
||||||
return current
|
return current
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user