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:
|
||||
name: Online Accounts
|
||||
description: Discover my online presence.
|
||||
name: Accounts
|
||||
description: My Online Accounts
|
||||
icon:
|
||||
class: fa-solid fa-users
|
||||
children:
|
||||
- name: Channels
|
||||
description: Platforms where I share content.
|
||||
subitems:
|
||||
- name: Publications
|
||||
description: My Publications
|
||||
icon:
|
||||
class: fas fa-newspaper
|
||||
children:
|
||||
- name: Microblogs
|
||||
description: Stay updated with my microblog posts.
|
||||
icon:
|
||||
class: fa-solid fa-pen-nib
|
||||
children:
|
||||
- name: Mastodon
|
||||
description: Follow my updates on Mastodon.
|
||||
subitems:
|
||||
- name: Microblog
|
||||
description: Read my microblogs
|
||||
icon:
|
||||
class: fa-brands fa-mastodon
|
||||
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
|
||||
description: View my photography.
|
||||
icon:
|
||||
class: fa-solid fa-images
|
||||
children:
|
||||
subitems:
|
||||
- name: Pixelfed
|
||||
description: Explore my photo gallery on Pixelfed.
|
||||
description: View my photo gallery
|
||||
icon:
|
||||
class: fa-solid fa-camera
|
||||
url: https://s.veen.world/pictures
|
||||
- name: Instagram
|
||||
description: Follow me on Instagram.
|
||||
description: Follow me on Instagram
|
||||
icon:
|
||||
class: fa-brands fa-instagram
|
||||
url: https://www.instagram.com/kevinveenbirkenbach/
|
||||
identifier: kevinveenbirkenbach
|
||||
warning: Platforms by Meta (e.g., Instagram, Facebook) may compromise your data privacy. Consider using decentralized alternatives.
|
||||
alternatives:
|
||||
- link: accounts.channels.pictures.pixelfed
|
||||
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.
|
||||
|
||||
- name: Videos
|
||||
description: Watch my video content.
|
||||
icon:
|
||||
class: fa-solid fa-video
|
||||
children:
|
||||
- name: Peertube
|
||||
description: Discover my videos on Peertube.
|
||||
description: Watch my videos
|
||||
icon:
|
||||
class: fa-solid fa-video
|
||||
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
|
||||
description: Read my articles and stories.
|
||||
description: Read my blog
|
||||
icon:
|
||||
class: fa-solid fa-blog
|
||||
url: https://blog.veen.world
|
||||
|
||||
- name: Code
|
||||
description: Access my coding projects.
|
||||
icon:
|
||||
class: fa-solid fa-laptop-code
|
||||
children:
|
||||
- name: GitHub
|
||||
description: View my GitHub repositories.
|
||||
description: Check out my Code
|
||||
subitems:
|
||||
- name: Github
|
||||
description: View my GitHub profile
|
||||
icon:
|
||||
class: bi bi-github
|
||||
url: https://github.com/kevinveenbirkenbach
|
||||
|
||||
- name: Gitea
|
||||
description: Explore my self-hosted repositories.
|
||||
description: Explore my code repositories
|
||||
icon:
|
||||
class: fa-solid fa-code
|
||||
url: https://git.veen.world/kevinveenbirkenbach
|
||||
|
||||
- name: Social Media
|
||||
description: Social and developer platforms.
|
||||
description: Social and developer networks
|
||||
icon:
|
||||
class: fa-brands fa-meta
|
||||
children:
|
||||
url:
|
||||
subitems:
|
||||
- name: Facebook
|
||||
description: Visit my Facebook page.
|
||||
description: Like my Facebook page
|
||||
icon:
|
||||
class: fa-brands fa-facebook
|
||||
url: https://www.facebook.com/kevinveenbirkenbach
|
||||
|
||||
- link: navigation.header.contact.messenger
|
||||
|
||||
- name: Career Profiles
|
||||
description: Professional networking profiles.
|
||||
- name: Carreer Profiles
|
||||
icon:
|
||||
class: fa-solid fa-user-tie
|
||||
children:
|
||||
subitems:
|
||||
- name: XING
|
||||
description: View my XING profile.
|
||||
description: Visit my XING profile
|
||||
icon:
|
||||
class: bi bi-building
|
||||
url: https://www.xing.com/profile/Kevin_VeenBirkenbach
|
||||
|
||||
- name: LinkedIn
|
||||
description: Connect with me on LinkedIn.
|
||||
description: Connect on LinkedIn
|
||||
icon:
|
||||
class: bi bi-linkedin
|
||||
url: https://www.linkedin.com/in/kevinveenbirkenbach
|
||||
|
||||
- name: Sports
|
||||
description: My sports activities and logs.
|
||||
description: My sport activities
|
||||
icon:
|
||||
class: fa-solid fa-running
|
||||
children:
|
||||
url:
|
||||
subitems:
|
||||
- name: Garmin
|
||||
description: Explore my Garmin activity records.
|
||||
description: My Garmin activities
|
||||
icon:
|
||||
class: fa-solid fa-person-running
|
||||
url: https://s.veen.world/garmin
|
||||
|
||||
- name: Eversports
|
||||
description: View my Eversports sessions.
|
||||
description: My Eversports sessions
|
||||
icon:
|
||||
class: fa-solid fa-dumbbell
|
||||
url: https://s.veen.world/eversports
|
||||
|
||||
- name: Duolingo
|
||||
description: Join me in language learning.
|
||||
description: Learn with me on Duolingo
|
||||
icon:
|
||||
class: fa-solid fa-language
|
||||
url: https://www.duolingo.com/profile/kevinbirkenbach
|
||||
|
||||
- name: Spotify
|
||||
description: Listen to my playlists.
|
||||
description: Listen to my playlists
|
||||
icon:
|
||||
class: fa-brands fa-spotify
|
||||
url: https://open.spotify.com/user/31vebfzbjf3p7oualis76qfpr5ty
|
||||
|
||||
- name: Patreon
|
||||
description: Support my work on Patreon.
|
||||
description: Support me on Patreon
|
||||
icon:
|
||||
class: fa-brands fa-patreon
|
||||
url: https://patreon.com/kevinveenbirkenbach
|
||||
|
||||
- name: Discourse
|
||||
description: Join discussions on my forum.
|
||||
description: Follow me on Discourse
|
||||
icon:
|
||||
class: fa-brands fa-discourse
|
||||
url: https://forum.veen.world/u/kevinveenbirkenbach
|
||||
|
||||
|
||||
cards:
|
||||
- icon:
|
||||
source: https://cloud.veen.world/s/logo_agile_coach_512x512/download
|
||||
@ -286,18 +251,16 @@ company:
|
||||
imprint_url: https://s.veen.world/imprint
|
||||
navigation:
|
||||
header:
|
||||
children:
|
||||
- link: accounts.channels.children
|
||||
- name: Contact
|
||||
description: Get in touch
|
||||
icon:
|
||||
class: fa-solid fa-envelope
|
||||
children:
|
||||
subitems:
|
||||
- name: Email
|
||||
description: Send me an email
|
||||
icon:
|
||||
class: fa-solid fa-envelope
|
||||
children:
|
||||
subitems:
|
||||
- name: Email
|
||||
description: Send me an email
|
||||
icon:
|
||||
@ -335,7 +298,7 @@ navigation:
|
||||
description: Social and developer networks
|
||||
icon:
|
||||
class: fa-solid fa-comments
|
||||
children:
|
||||
subitems:
|
||||
- name: Matrix
|
||||
description: Chat with me on Matrix
|
||||
icon:
|
||||
@ -382,20 +345,20 @@ navigation:
|
||||
- link: navigation.header.contact.messenger.matrix
|
||||
- link: navigation.header.contact.messenger.signal
|
||||
- link: navigation.header.contact.messenger.telegram
|
||||
|
||||
footer:
|
||||
children:
|
||||
- link: accounts
|
||||
- name: Solution Hub
|
||||
description: Curated collection of self hosted tools
|
||||
icon:
|
||||
class: fa-solid fa-network-wired
|
||||
url:
|
||||
children:
|
||||
subitems:
|
||||
- name: Community
|
||||
description: Tools to manage the community
|
||||
icon:
|
||||
class: fa-solid fa-users
|
||||
children:
|
||||
subitems:
|
||||
- name: Forum
|
||||
description: Join the discussion
|
||||
icon:
|
||||
@ -415,7 +378,7 @@ navigation:
|
||||
description: Project Management Tools
|
||||
icon:
|
||||
class: fa-solid fa-chart-line
|
||||
children:
|
||||
subitems:
|
||||
- name: Open Project
|
||||
description: Explore my projects
|
||||
icon:
|
||||
@ -431,7 +394,7 @@ navigation:
|
||||
- name: Communication
|
||||
icon:
|
||||
class: fa-solid fa-comments
|
||||
children:
|
||||
subitems:
|
||||
- name: Elements
|
||||
description: Chat with me
|
||||
icon:
|
||||
@ -452,7 +415,7 @@ navigation:
|
||||
- name: Tools
|
||||
icon:
|
||||
class: fas fa-tools
|
||||
children:
|
||||
subitems:
|
||||
- name: Matomo
|
||||
description: Analyze with Matomo
|
||||
icon:
|
||||
@ -480,12 +443,12 @@ navigation:
|
||||
description: All information about me
|
||||
icon:
|
||||
class: fa-solid fa-user
|
||||
children:
|
||||
subitems:
|
||||
- name: Logbooks
|
||||
description: Access my personal logbooks (diving, flying, sailing)
|
||||
icon:
|
||||
class: fa-solid fa-book
|
||||
children:
|
||||
subitems:
|
||||
- name: Skydiver
|
||||
description: View my skydiving logs
|
||||
icon:
|
||||
@ -520,21 +483,11 @@ navigation:
|
||||
icon:
|
||||
class: fa-solid fa-file-lines
|
||||
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
|
||||
description: Access my certifications, degrees, and professional records
|
||||
icon:
|
||||
class: fa-solid fa-id-card
|
||||
children:
|
||||
subitems:
|
||||
- name: Degrees
|
||||
description: View my academic degrees
|
||||
icon:
|
||||
@ -550,15 +503,10 @@ navigation:
|
||||
icon:
|
||||
class: fa-solid fa-scroll
|
||||
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
|
||||
|
||||
- name: Imprint
|
||||
description: Check out the imprint information
|
||||
icon:
|
||||
class: fa-solid fa-scale-balanced
|
||||
url: https://s.veen.world/imprint
|
||||
|
@ -23,17 +23,13 @@ a {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Subtle shadow effect */
|
||||
.navbar, .card, .dropdown-menu{
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.navbar, .card {
|
||||
flex: 1; /* Ensures cards fill the height of their container */
|
||||
border-radius: 5px; /* Rounded corners */
|
||||
border: 1px solid #ccc; /* Optional border color */
|
||||
padding: 10px; /* Inner spacing */
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); /* Subtle shadow effect */
|
||||
color: #000000 !important;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
@ -3,13 +3,34 @@
|
||||
display: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
width: max-content !important; /* Passt die Breite an das breiteste Item an */
|
||||
box-sizing: border-box; /* Berücksichtigt Innenabstand und Rahmen */
|
||||
}
|
||||
|
||||
/* Positionierung von Submenüs */
|
||||
.dropdown-submenu > .dropdown-menu {
|
||||
position: absolute;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
.dropdown-submenu > .dropdown-menu {
|
||||
position: absolute;
|
||||
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 subMenuItems = document.querySelectorAll('.dropdown-submenu');
|
||||
|
||||
function addMenuEventListeners(items, isTopLevel) {
|
||||
items.forEach(item => {
|
||||
menuItems.forEach(item => {
|
||||
let timeout;
|
||||
|
||||
function onMouseEnter() {
|
||||
clearTimeout(timeout);
|
||||
openMenu(item, isTopLevel);
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
timeout = setTimeout(() => {
|
||||
closeMenu(item);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Öffnen beim Hovern
|
||||
item.addEventListener('mouseenter', onMouseEnter);
|
||||
item.addEventListener('mouseenter', () => {
|
||||
clearTimeout(timeout);
|
||||
openMenu(item, true);
|
||||
});
|
||||
|
||||
// Verzögertes Schließen beim Verlassen
|
||||
item.addEventListener('mouseleave', onMouseLeave);
|
||||
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, isTopLevel);
|
||||
openMenu(item, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addAllMenuEventListeners() {
|
||||
const updatedMenuItems = document.querySelectorAll('.nav-item.dropdown');
|
||||
const updatedSubMenuItems = document.querySelectorAll('.dropdown-submenu');
|
||||
addMenuEventListeners(updatedMenuItems, true);
|
||||
addMenuEventListeners(updatedSubMenuItems, false);
|
||||
}
|
||||
subMenuItems.forEach(item => {
|
||||
let timeout;
|
||||
|
||||
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
|
||||
document.addEventListener('click', () => {
|
||||
@ -53,10 +63,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
item.classList.add('open');
|
||||
const submenu = item.querySelector('.dropdown-menu');
|
||||
if (submenu) {
|
||||
adjustMenuPosition(submenu, item, isTopLevel);
|
||||
submenu.style.display = 'block';
|
||||
submenu.style.opacity = '1';
|
||||
submenu.style.visibility = 'visible';
|
||||
adjustMenuPosition(submenu, item, isTopLevel);
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +99,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (isTopLevel) {
|
||||
// Top-Level-Menüs öffnen nur nach oben oder unten
|
||||
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';
|
||||
} else {
|
||||
submenu.style.top = `${parentRect.height}px`;
|
||||
@ -101,14 +111,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
submenu.style.left = prefersRight ? '100%' : 'auto';
|
||||
submenu.style.right = prefersRight ? 'auto' : '100%';
|
||||
|
||||
// Öffnen nach oben, wenn unten kein Platz ist
|
||||
if (spaceBelow < rect.height && spaceAbove > rect.height) {
|
||||
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';
|
||||
}
|
||||
const prefersBelow = spaceBelow >= spaceAbove;
|
||||
submenu.style.top = prefersBelow ? '0' : 'auto';
|
||||
submenu.style.bottom = prefersBelow ? 'auto' : '100%';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,33 +1,33 @@
|
||||
{% macro render_icon_and_name(item) %}
|
||||
<i class="{{ item.icon.class if item.icon is defined and item.icon.class is defined else 'fa-solid fa-link' }}"></i>
|
||||
{% if item.name is defined %}
|
||||
{{ item.name }}
|
||||
{% else %}
|
||||
Unnamed Item: {{item}}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
<!-- Template for children -->
|
||||
{% macro render_children(children) %}
|
||||
{% for children in children %}
|
||||
{% if children.children %}
|
||||
<!-- Template for Subitems -->
|
||||
{% macro render_subitems(subitems) %}
|
||||
{% for subitem in subitems %}
|
||||
{% if subitem.subitems %}
|
||||
<li class="dropdown-submenu position-relative">
|
||||
<a class="dropdown-item dropdown-toggle" title="{{ children.description }}">
|
||||
{{ render_icon_and_name(children) }}
|
||||
<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 %}
|
||||
<p>Missing icon in subitem: {{ subitem }}</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{{ render_children(children.children) }}
|
||||
{{ render_subitems(subitem.subitems) }}
|
||||
</ul>
|
||||
</li>
|
||||
{% elif children.identifier or children.warning or children.info %}
|
||||
{% elif subitem.identifier or subitem.warning or subitem.info %}
|
||||
<li>
|
||||
<a class="dropdown-item" onclick='openDynamicPopup({{ children|tojson|safe }})' data-bs-toggle="tooltip" title="{{ children.description }}">
|
||||
{{ render_icon_and_name(children) }}
|
||||
<a class="dropdown-item" onclick='openDynamicPopup({{ subitem|tojson|safe }})' data-bs-toggle="tooltip" title="{{ subitem.description }}">
|
||||
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ children.url }}" target="{{ children.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ children.description }}">
|
||||
{{ render_icon_and_name(children) }}
|
||||
<a class="dropdown-item" href="{{ subitem.url }}" target="{{ subitem.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ subitem.description }}">
|
||||
{% 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>
|
||||
</li>
|
||||
{% endif %}
|
||||
@ -42,26 +42,26 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
|
||||
<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 %}
|
||||
<!-- Single Item -->
|
||||
<li class="nav-item">
|
||||
<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>
|
||||
</li>
|
||||
{% else %}
|
||||
<!-- Dropdown Menu -->
|
||||
<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 %}
|
||||
{{ render_icon_and_name(item) }}
|
||||
<i class="{{ item.icon.class }}"></i> {{ item.name }}
|
||||
{% else %}
|
||||
<p>Missing icon in item: {{ item }}</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{{ render_children(item.children) }}
|
||||
{{ render_subitems(item.subitems) }}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
@ -2,7 +2,7 @@ from pprint import pprint
|
||||
class ConfigurationResolver:
|
||||
"""
|
||||
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):
|
||||
@ -14,56 +14,23 @@ class ConfigurationResolver:
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Recursively resolves `link` entries in the configuration.
|
||||
"""
|
||||
if isinstance(current_config, dict):
|
||||
for key, value in list(current_config.items()):
|
||||
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":
|
||||
if key == "link":
|
||||
try:
|
||||
loaded = self._find_entry(root_config, value.lower(), True)
|
||||
if isinstance(loaded, list) and len(loaded) > 2:
|
||||
loaded = self._find_entry(root_config, value.lower(), False)
|
||||
target = self._find_entry(root_config, value.lower(), True)
|
||||
if isinstance(target, list) and len(target) > 2:
|
||||
target = self._find_entry(root_config, value.lower(), False)
|
||||
current_config.clear()
|
||||
current_config.update(loaded)
|
||||
current_config.update(target)
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
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:
|
||||
self._recursive_resolve(value, root_config)
|
||||
@ -71,9 +38,9 @@ class ConfigurationResolver:
|
||||
for item in current_config:
|
||||
self._recursive_resolve(item, root_config)
|
||||
|
||||
def _get_children(self,current):
|
||||
if isinstance(current, dict) and ("children" in current and current["children"]):
|
||||
current = current["children"]
|
||||
def _get_subitems(self,current):
|
||||
if isinstance(current, dict) and ("subitems" in current and current["subitems"]):
|
||||
current = current["subitems"]
|
||||
return current
|
||||
|
||||
def _find_by_name(self,current, part):
|
||||
@ -82,21 +49,18 @@ class ConfigurationResolver:
|
||||
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.
|
||||
Supports both dictionaries and lists with `children` navigation.
|
||||
Supports both dictionaries and lists with `subitems` navigation.
|
||||
"""
|
||||
parts = path.split('.')
|
||||
current = config
|
||||
for part in parts:
|
||||
if isinstance(current, list):
|
||||
# If children explicit declared just load children
|
||||
if part != "children":
|
||||
# Look for a matching name in the list
|
||||
found = self._find_by_name(current,part)
|
||||
if found:
|
||||
current = found
|
||||
print(
|
||||
f"Matching entry for '{part}' in list. Path so far: {' > '.join(parts[:parts.index(part)+1])}. "
|
||||
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"Current list: {current}"
|
||||
)
|
||||
current = found
|
||||
elif isinstance(current, dict):
|
||||
# Case-insensitive dictionary lookup
|
||||
key = next((k for k in current if k.lower() == part), None)
|
||||
if key is None:
|
||||
current = self._find_by_name(current["children"],part)
|
||||
current = self._find_by_name(current["subitems"],part)
|
||||
if not current:
|
||||
raise KeyError(
|
||||
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"Path so far: {' > '.join(parts[:parts.index(part)+1])}"
|
||||
)
|
||||
if children:
|
||||
current = self._get_children(current)
|
||||
if subitems:
|
||||
current = self._get_subitems(current)
|
||||
|
||||
return current
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user