mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2025-06-29 08:12:01 +02:00
Compare commits
13 Commits
00e0096f8a
...
3acf7d36a4
Author | SHA1 | Date | |
---|---|---|---|
3acf7d36a4 | |||
2e89e8c31e | |||
ef0d98cdd1 | |||
9f143e39b4 | |||
8ad3ca54cc | |||
9ff356ba70 | |||
c01e9125aa | |||
c24e35c4e8 | |||
b74ff2da78 | |||
066f10edfc | |||
abdaf54147 | |||
ac0b1e9a14 | |||
f9d5a90f94 |
158
app/config.yaml
158
app/config.yaml
@ -1,137 +1,172 @@
|
|||||||
---
|
---
|
||||||
accounts:
|
accounts:
|
||||||
name: Accounts
|
name: Online Accounts
|
||||||
description: My Online Accounts
|
description: Discover my online presence.
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-users
|
class: fa-solid fa-users
|
||||||
subitems:
|
children:
|
||||||
- name: Publications
|
- name: Channels
|
||||||
description: My Publications
|
description: Platforms where I share content.
|
||||||
icon:
|
icon:
|
||||||
class: fas fa-newspaper
|
class: fas fa-newspaper
|
||||||
subitems:
|
children:
|
||||||
- name: Microblog
|
- name: Microblogs
|
||||||
description: Read my microblogs
|
description: Stay updated with my microblog posts.
|
||||||
|
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
|
||||||
subitems:
|
children:
|
||||||
- name: Pixelfed
|
- name: Pixelfed
|
||||||
description: View my photo gallery
|
description: Explore my photo gallery on Pixelfed.
|
||||||
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: 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.
|
warning: Platforms by Meta (e.g., Instagram, Facebook) may compromise your data privacy. Consider using decentralized alternatives.
|
||||||
|
alternatives:
|
||||||
|
- link: accounts.channels.pictures.pixelfed
|
||||||
|
|
||||||
- name: Videos
|
- name: Videos
|
||||||
description: Watch my videos
|
description: Watch my video content.
|
||||||
|
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 blog
|
description: Read my articles and stories.
|
||||||
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
|
||||||
description: Check out my Code
|
children:
|
||||||
subitems:
|
- name: GitHub
|
||||||
- name: Github
|
description: View my GitHub repositories.
|
||||||
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 code repositories
|
description: Explore my self-hosted 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 networks
|
description: Social and developer platforms.
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-meta
|
class: fa-brands fa-meta
|
||||||
url:
|
children:
|
||||||
subitems:
|
|
||||||
- name: Facebook
|
- name: Facebook
|
||||||
description: Like my Facebook page
|
description: Visit 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
|
||||||
subitems:
|
children:
|
||||||
- name: XING
|
- name: XING
|
||||||
description: Visit my XING profile
|
description: View 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 on LinkedIn
|
description: Connect with me 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 sport activities
|
description: My sports activities and logs.
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-running
|
class: fa-solid fa-running
|
||||||
url:
|
children:
|
||||||
subitems:
|
|
||||||
- name: Garmin
|
- name: Garmin
|
||||||
description: My Garmin activities
|
description: Explore my Garmin activity records.
|
||||||
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: My Eversports sessions
|
description: View 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: Learn with me on Duolingo
|
description: Join me in language learning.
|
||||||
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 me on Patreon
|
description: Support my work 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: Follow me on Discourse
|
description: Join discussions on my forum.
|
||||||
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
|
||||||
@ -251,16 +286,18 @@ 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
|
||||||
subitems:
|
children:
|
||||||
- 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
|
||||||
subitems:
|
children:
|
||||||
- name: Email
|
- name: Email
|
||||||
description: Send me an email
|
description: Send me an email
|
||||||
icon:
|
icon:
|
||||||
@ -298,7 +335,7 @@ navigation:
|
|||||||
description: Social and developer networks
|
description: Social and developer networks
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-comments
|
class: fa-solid fa-comments
|
||||||
subitems:
|
children:
|
||||||
- name: Matrix
|
- name: Matrix
|
||||||
description: Chat with me on Matrix
|
description: Chat with me on Matrix
|
||||||
icon:
|
icon:
|
||||||
@ -345,20 +382,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:
|
||||||
subitems:
|
children:
|
||||||
- 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
|
||||||
subitems:
|
children:
|
||||||
- name: Forum
|
- name: Forum
|
||||||
description: Join the discussion
|
description: Join the discussion
|
||||||
icon:
|
icon:
|
||||||
@ -378,7 +415,7 @@ navigation:
|
|||||||
description: Project Management Tools
|
description: Project Management Tools
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-chart-line
|
class: fa-solid fa-chart-line
|
||||||
subitems:
|
children:
|
||||||
- name: Open Project
|
- name: Open Project
|
||||||
description: Explore my projects
|
description: Explore my projects
|
||||||
icon:
|
icon:
|
||||||
@ -394,7 +431,7 @@ navigation:
|
|||||||
- name: Communication
|
- name: Communication
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-comments
|
class: fa-solid fa-comments
|
||||||
subitems:
|
children:
|
||||||
- name: Elements
|
- name: Elements
|
||||||
description: Chat with me
|
description: Chat with me
|
||||||
icon:
|
icon:
|
||||||
@ -415,7 +452,7 @@ navigation:
|
|||||||
- name: Tools
|
- name: Tools
|
||||||
icon:
|
icon:
|
||||||
class: fas fa-tools
|
class: fas fa-tools
|
||||||
subitems:
|
children:
|
||||||
- name: Matomo
|
- name: Matomo
|
||||||
description: Analyze with Matomo
|
description: Analyze with Matomo
|
||||||
icon:
|
icon:
|
||||||
@ -443,12 +480,12 @@ navigation:
|
|||||||
description: All information about me
|
description: All information about me
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-user
|
class: fa-solid fa-user
|
||||||
subitems:
|
children:
|
||||||
- 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
|
||||||
subitems:
|
children:
|
||||||
- name: Skydiver
|
- name: Skydiver
|
||||||
description: View my skydiving logs
|
description: View my skydiving logs
|
||||||
icon:
|
icon:
|
||||||
@ -483,11 +520,21 @@ 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
|
||||||
subitems:
|
children:
|
||||||
- name: Degrees
|
- name: Degrees
|
||||||
description: View my academic degrees
|
description: View my academic degrees
|
||||||
icon:
|
icon:
|
||||||
@ -503,10 +550,15 @@ 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,13 +23,17 @@ 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,34 +3,13 @@
|
|||||||
display: none;
|
display: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
width: max-content !important; /* Passt die Breite an das breiteste Item an */
|
||||||
}
|
box-sizing: border-box; /* Berücksichtigt Innenabstand und Rahmen */
|
||||||
|
}
|
||||||
|
|
||||||
/* Dropdown-Menü beim Hover anzeigen */
|
/* Positionierung von Submenüs */
|
||||||
.nav-item.dropdown:hover > .dropdown-menu,
|
.dropdown-submenu > .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 */
|
|
||||||
.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,57 +2,47 @@ 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');
|
||||||
|
|
||||||
menuItems.forEach(item => {
|
function addMenuEventListeners(items, isTopLevel) {
|
||||||
|
items.forEach(item => {
|
||||||
let timeout;
|
let timeout;
|
||||||
|
|
||||||
// Öffnen beim Hovern
|
function onMouseEnter() {
|
||||||
item.addEventListener('mouseenter', () => {
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
openMenu(item, true);
|
openMenu(item, isTopLevel);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function onMouseLeave() {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
closeMenu(item);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Öffnen beim Hovern
|
||||||
|
item.addEventListener('mouseenter', onMouseEnter);
|
||||||
|
|
||||||
// Verzögertes Schließen beim Verlassen
|
// Verzögertes Schließen beim Verlassen
|
||||||
item.addEventListener('mouseleave', () => {
|
item.addEventListener('mouseleave', onMouseLeave);
|
||||||
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, true);
|
openMenu(item, isTopLevel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
subMenuItems.forEach(item => {
|
|
||||||
let timeout;
|
|
||||||
|
|
||||||
// Ö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);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
function addAllMenuEventListeners() {
|
||||||
|
const updatedMenuItems = document.querySelectorAll('.nav-item.dropdown');
|
||||||
|
const updatedSubMenuItems = document.querySelectorAll('.dropdown-submenu');
|
||||||
|
addMenuEventListeners(updatedMenuItems, true);
|
||||||
|
addMenuEventListeners(updatedSubMenuItems, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAllMenuEventListeners();
|
||||||
|
|
||||||
// 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', () => {
|
||||||
@ -63,10 +53,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +89,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 = '100%';
|
submenu.style.bottom = `${window.innerHeight - parentRect.bottom - parentRect.height}px`;
|
||||||
submenu.style.top = 'auto';
|
submenu.style.top = 'auto';
|
||||||
} else {
|
} else {
|
||||||
submenu.style.top = `${parentRect.height}px`;
|
submenu.style.top = `${parentRect.height}px`;
|
||||||
@ -111,9 +101,14 @@ 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%';
|
||||||
|
|
||||||
const prefersBelow = spaceBelow >= spaceAbove;
|
// Öffnen nach oben, wenn unten kein Platz ist
|
||||||
submenu.style.top = prefersBelow ? '0' : 'auto';
|
if (spaceBelow < rect.height && spaceAbove > rect.height) {
|
||||||
submenu.style.bottom = prefersBelow ? 'auto' : '100%';
|
submenu.style.top = 'auto';
|
||||||
|
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 @@
|
|||||||
<!-- Template for Subitems -->
|
{% macro render_icon_and_name(item) %}
|
||||||
{% macro render_subitems(subitems) %}
|
<i class="{{ item.icon.class if item.icon is defined and item.icon.class is defined else 'fa-solid fa-link' }}"></i>
|
||||||
{% for subitem in subitems %}
|
{% if item.name is defined %}
|
||||||
{% if subitem.subitems %}
|
{{ item.name }}
|
||||||
<li class="dropdown-submenu position-relative">
|
|
||||||
<a class="dropdown-item dropdown-toggle" href="#" title="{{ subitem.description }}">
|
|
||||||
{% if subitem.icon is defined and subitem.icon.class is defined %}
|
|
||||||
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Missing icon in subitem: {{ subitem }}</p>
|
Unnamed Item: {{item}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
<!-- Template for children -->
|
||||||
|
{% macro render_children(children) %}
|
||||||
|
{% for children in children %}
|
||||||
|
{% if children.children %}
|
||||||
|
<li class="dropdown-submenu position-relative">
|
||||||
|
<a class="dropdown-item dropdown-toggle" title="{{ children.description }}">
|
||||||
|
{{ render_icon_and_name(children) }}
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{{ render_subitems(subitem.subitems) }}
|
{{ render_children(children.children) }}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% elif subitem.identifier or subitem.warning or subitem.info %}
|
{% elif children.identifier or children.warning or children.info %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" onclick='openDynamicPopup({{ subitem|tojson|safe }})' data-bs-toggle="tooltip" title="{{ subitem.description }}">
|
<a class="dropdown-item" onclick='openDynamicPopup({{ children|tojson|safe }})' data-bs-toggle="tooltip" title="{{ children.description }}">
|
||||||
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
|
{{ render_icon_and_name(children) }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{{ subitem.url }}" target="{{ subitem.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ subitem.description }}">
|
<a class="dropdown-item" href="{{ children.url }}" target="{{ children.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ children.description }}">
|
||||||
{% if subitem.icon is defined and subitem.icon.class is defined %}
|
{{ render_icon_and_name(children) }}
|
||||||
<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] %}
|
{% for item in navigation[menu_type].children %}
|
||||||
{% 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 }}">
|
||||||
<i class="{{ item.icon.class }}"></i> {{ item.name }}
|
{{ render_icon_and_name(item) }}
|
||||||
</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" href="#" id="navbarDropdown{{ loop.index }}" role="button" data-bs-toggle="dropdown" data-bs-display="dynamic" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" 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 %}
|
||||||
<i class="{{ item.icon.class }}"></i> {{ item.name }}
|
{{ render_icon_and_name(item) }}
|
||||||
{% 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_subitems(item.subitems) }}
|
{{ render_children(item.children) }}
|
||||||
</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 `subitems`.
|
Supports navigation through dictionaries, lists, and `children`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
@ -14,23 +14,56 @@ 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 == "link":
|
if key == "children":
|
||||||
|
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:
|
||||||
target = self._find_entry(root_config, value.lower(), True)
|
loaded = self._find_entry(root_config, value.lower(), True)
|
||||||
if isinstance(target, list) and len(target) > 2:
|
if isinstance(loaded, list) and len(loaded) > 2:
|
||||||
target = self._find_entry(root_config, value.lower(), False)
|
loaded = self._find_entry(root_config, value.lower(), False)
|
||||||
current_config.clear()
|
current_config.clear()
|
||||||
current_config.update(target)
|
current_config.update(loaded)
|
||||||
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"Current path: {key}, Current config: {current_config}" + (f", Loaded: {loaded}" if 'loaded' in locals() or 'loaded' in globals() else "")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._recursive_resolve(value, root_config)
|
self._recursive_resolve(value, root_config)
|
||||||
@ -38,9 +71,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_subitems(self,current):
|
def _get_children(self,current):
|
||||||
if isinstance(current, dict) and ("subitems" in current and current["subitems"]):
|
if isinstance(current, dict) and ("children" in current and current["children"]):
|
||||||
current = current["subitems"]
|
current = current["children"]
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def _find_by_name(self,current, part):
|
def _find_by_name(self,current, part):
|
||||||
@ -49,18 +82,21 @@ class ConfigurationResolver:
|
|||||||
None
|
None
|
||||||
)
|
)
|
||||||
|
|
||||||
def _find_entry(self, config, path, subitems):
|
def _find_entry(self, config, path, children):
|
||||||
"""
|
"""
|
||||||
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 `subitems` navigation.
|
Supports both dictionaries and lists with `children` 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}"
|
||||||
@ -70,12 +106,11 @@ 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["subitems"],part)
|
current = self._find_by_name(current["children"],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])}. "
|
||||||
@ -89,8 +124,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 subitems:
|
if children:
|
||||||
current = self._get_subitems(current)
|
current = self._get_children(current)
|
||||||
|
|
||||||
return current
|
return current
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user