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:
|
||||
name: Accounts
|
||||
description: My Online Accounts
|
||||
name: Online Accounts
|
||||
description: Discover my online presence.
|
||||
icon:
|
||||
class: fa-solid fa-users
|
||||
subitems:
|
||||
- name: Publications
|
||||
description: My Publications
|
||||
children:
|
||||
- name: Channels
|
||||
description: Platforms where I share content.
|
||||
icon:
|
||||
class: fas fa-newspaper
|
||||
subitems:
|
||||
- name: Microblog
|
||||
description: Read my microblogs
|
||||
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.
|
||||
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
|
||||
subitems:
|
||||
children:
|
||||
- name: Pixelfed
|
||||
description: View my photo gallery
|
||||
description: Explore my photo gallery on Pixelfed.
|
||||
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: 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
|
||||
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:
|
||||
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 blog
|
||||
description: Read my articles and stories.
|
||||
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
|
||||
description: Check out my Code
|
||||
subitems:
|
||||
- name: Github
|
||||
description: View my GitHub profile
|
||||
children:
|
||||
- name: GitHub
|
||||
description: View my GitHub repositories.
|
||||
icon:
|
||||
class: bi bi-github
|
||||
url: https://github.com/kevinveenbirkenbach
|
||||
|
||||
- name: Gitea
|
||||
description: Explore my code repositories
|
||||
description: Explore my self-hosted repositories.
|
||||
icon:
|
||||
class: fa-solid fa-code
|
||||
url: https://git.veen.world/kevinveenbirkenbach
|
||||
|
||||
- name: Social Media
|
||||
description: Social and developer networks
|
||||
description: Social and developer platforms.
|
||||
icon:
|
||||
class: fa-brands fa-meta
|
||||
url:
|
||||
subitems:
|
||||
children:
|
||||
- name: Facebook
|
||||
description: Like my Facebook page
|
||||
description: Visit my Facebook page.
|
||||
icon:
|
||||
class: fa-brands fa-facebook
|
||||
url: https://www.facebook.com/kevinveenbirkenbach
|
||||
|
||||
- link: navigation.header.contact.messenger
|
||||
- name: Carreer Profiles
|
||||
|
||||
- name: Career Profiles
|
||||
description: Professional networking profiles.
|
||||
icon:
|
||||
class: fa-solid fa-user-tie
|
||||
subitems:
|
||||
children:
|
||||
- name: XING
|
||||
description: Visit my XING profile
|
||||
description: View my XING profile.
|
||||
icon:
|
||||
class: bi bi-building
|
||||
url: https://www.xing.com/profile/Kevin_VeenBirkenbach
|
||||
|
||||
- name: LinkedIn
|
||||
description: Connect on LinkedIn
|
||||
description: Connect with me on LinkedIn.
|
||||
icon:
|
||||
class: bi bi-linkedin
|
||||
url: https://www.linkedin.com/in/kevinveenbirkenbach
|
||||
|
||||
- name: Sports
|
||||
description: My sport activities
|
||||
description: My sports activities and logs.
|
||||
icon:
|
||||
class: fa-solid fa-running
|
||||
url:
|
||||
subitems:
|
||||
children:
|
||||
- name: Garmin
|
||||
description: My Garmin activities
|
||||
description: Explore my Garmin activity records.
|
||||
icon:
|
||||
class: fa-solid fa-person-running
|
||||
url: https://s.veen.world/garmin
|
||||
|
||||
- name: Eversports
|
||||
description: My Eversports sessions
|
||||
description: View my Eversports sessions.
|
||||
icon:
|
||||
class: fa-solid fa-dumbbell
|
||||
url: https://s.veen.world/eversports
|
||||
|
||||
- name: Duolingo
|
||||
description: Learn with me on Duolingo
|
||||
description: Join me in language learning.
|
||||
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 me on Patreon
|
||||
description: Support my work on Patreon.
|
||||
icon:
|
||||
class: fa-brands fa-patreon
|
||||
url: https://patreon.com/kevinveenbirkenbach
|
||||
|
||||
- name: Discourse
|
||||
description: Follow me on Discourse
|
||||
description: Join discussions on my forum.
|
||||
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
|
||||
@ -251,16 +286,18 @@ 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
|
||||
subitems:
|
||||
children:
|
||||
- name: Email
|
||||
description: Send me an email
|
||||
icon:
|
||||
class: fa-solid fa-envelope
|
||||
subitems:
|
||||
children:
|
||||
- name: Email
|
||||
description: Send me an email
|
||||
icon:
|
||||
@ -298,7 +335,7 @@ navigation:
|
||||
description: Social and developer networks
|
||||
icon:
|
||||
class: fa-solid fa-comments
|
||||
subitems:
|
||||
children:
|
||||
- name: Matrix
|
||||
description: Chat with me on Matrix
|
||||
icon:
|
||||
@ -345,20 +382,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:
|
||||
subitems:
|
||||
children:
|
||||
- name: Community
|
||||
description: Tools to manage the community
|
||||
icon:
|
||||
class: fa-solid fa-users
|
||||
subitems:
|
||||
children:
|
||||
- name: Forum
|
||||
description: Join the discussion
|
||||
icon:
|
||||
@ -378,7 +415,7 @@ navigation:
|
||||
description: Project Management Tools
|
||||
icon:
|
||||
class: fa-solid fa-chart-line
|
||||
subitems:
|
||||
children:
|
||||
- name: Open Project
|
||||
description: Explore my projects
|
||||
icon:
|
||||
@ -394,7 +431,7 @@ navigation:
|
||||
- name: Communication
|
||||
icon:
|
||||
class: fa-solid fa-comments
|
||||
subitems:
|
||||
children:
|
||||
- name: Elements
|
||||
description: Chat with me
|
||||
icon:
|
||||
@ -415,7 +452,7 @@ navigation:
|
||||
- name: Tools
|
||||
icon:
|
||||
class: fas fa-tools
|
||||
subitems:
|
||||
children:
|
||||
- name: Matomo
|
||||
description: Analyze with Matomo
|
||||
icon:
|
||||
@ -443,12 +480,12 @@ navigation:
|
||||
description: All information about me
|
||||
icon:
|
||||
class: fa-solid fa-user
|
||||
subitems:
|
||||
children:
|
||||
- name: Logbooks
|
||||
description: Access my personal logbooks (diving, flying, sailing)
|
||||
icon:
|
||||
class: fa-solid fa-book
|
||||
subitems:
|
||||
children:
|
||||
- name: Skydiver
|
||||
description: View my skydiving logs
|
||||
icon:
|
||||
@ -483,11 +520,21 @@ 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
|
||||
subitems:
|
||||
children:
|
||||
- name: Degrees
|
||||
description: View my academic degrees
|
||||
icon:
|
||||
@ -503,10 +550,15 @@ 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,13 +23,17 @@ 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,34 +3,13 @@
|
||||
display: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
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;
|
||||
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;
|
||||
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 subMenuItems = document.querySelectorAll('.dropdown-submenu');
|
||||
|
||||
menuItems.forEach(item => {
|
||||
function addMenuEventListeners(items, isTopLevel) {
|
||||
items.forEach(item => {
|
||||
let timeout;
|
||||
|
||||
// Öffnen beim Hovern
|
||||
item.addEventListener('mouseenter', () => {
|
||||
function onMouseEnter() {
|
||||
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
|
||||
item.addEventListener('mouseleave', () => {
|
||||
timeout = setTimeout(() => closeMenu(item), 500);
|
||||
});
|
||||
item.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Ö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, 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
|
||||
document.addEventListener('click', () => {
|
||||
@ -63,10 +53,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +89,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 = '100%';
|
||||
submenu.style.bottom = `${window.innerHeight - parentRect.bottom - parentRect.height}px`;
|
||||
submenu.style.top = 'auto';
|
||||
} else {
|
||||
submenu.style.top = `${parentRect.height}px`;
|
||||
@ -111,9 +101,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
submenu.style.left = prefersRight ? '100%' : 'auto';
|
||||
submenu.style.right = prefersRight ? 'auto' : '100%';
|
||||
|
||||
const prefersBelow = spaceBelow >= spaceAbove;
|
||||
submenu.style.top = prefersBelow ? '0' : 'auto';
|
||||
submenu.style.bottom = prefersBelow ? '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';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,33 +1,33 @@
|
||||
<!-- 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" href="#" title="{{ subitem.description }}">
|
||||
{% if subitem.icon is defined and subitem.icon.class is defined %}
|
||||
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
|
||||
{% 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 %}
|
||||
<p>Missing icon in subitem: {{ subitem }}</p>
|
||||
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">
|
||||
<a class="dropdown-item dropdown-toggle" title="{{ children.description }}">
|
||||
{{ render_icon_and_name(children) }}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{{ render_subitems(subitem.subitems) }}
|
||||
{{ render_children(children.children) }}
|
||||
</ul>
|
||||
</li>
|
||||
{% elif subitem.identifier or subitem.warning or subitem.info %}
|
||||
{% elif children.identifier or children.warning or children.info %}
|
||||
<li>
|
||||
<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 class="dropdown-item" onclick='openDynamicPopup({{ children|tojson|safe }})' data-bs-toggle="tooltip" title="{{ children.description }}">
|
||||
{{ render_icon_and_name(children) }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<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 class="dropdown-item" href="{{ children.url }}" target="{{ children.target|default('_blank') }}" data-bs-toggle="tooltip" title="{{ children.description }}">
|
||||
{{ render_icon_and_name(children) }}
|
||||
</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] %}
|
||||
{% for item in navigation[menu_type].children %}
|
||||
{% 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 }}">
|
||||
<i class="{{ item.icon.class }}"></i> {{ item.name }}
|
||||
{{ render_icon_and_name(item) }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<!-- Dropdown Menu -->
|
||||
<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 %}
|
||||
<i class="{{ item.icon.class }}"></i> {{ item.name }}
|
||||
{{ render_icon_and_name(item) }}
|
||||
{% else %}
|
||||
<p>Missing icon in item: {{ item }}</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{{ render_subitems(item.subitems) }}
|
||||
{{ render_children(item.children) }}
|
||||
</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 `subitems`.
|
||||
Supports navigation through dictionaries, lists, and `children`.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
@ -14,23 +14,56 @@ 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 == "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:
|
||||
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)
|
||||
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)
|
||||
current_config.clear()
|
||||
current_config.update(target)
|
||||
current_config.update(loaded)
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
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:
|
||||
self._recursive_resolve(value, root_config)
|
||||
@ -38,9 +71,9 @@ class ConfigurationResolver:
|
||||
for item in current_config:
|
||||
self._recursive_resolve(item, root_config)
|
||||
|
||||
def _get_subitems(self,current):
|
||||
if isinstance(current, dict) and ("subitems" in current and current["subitems"]):
|
||||
current = current["subitems"]
|
||||
def _get_children(self,current):
|
||||
if isinstance(current, dict) and ("children" in current and current["children"]):
|
||||
current = current["children"]
|
||||
return current
|
||||
|
||||
def _find_by_name(self,current, part):
|
||||
@ -49,18 +82,21 @@ class ConfigurationResolver:
|
||||
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.
|
||||
Supports both dictionaries and lists with `subitems` navigation.
|
||||
Supports both dictionaries and lists with `children` 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}"
|
||||
@ -70,12 +106,11 @@ 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["subitems"],part)
|
||||
current = self._find_by_name(current["children"],part)
|
||||
if not current:
|
||||
raise KeyError(
|
||||
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"Path so far: {' > '.join(parts[:parts.index(part)+1])}"
|
||||
)
|
||||
if subitems:
|
||||
current = self._get_subitems(current)
|
||||
if children:
|
||||
current = self._get_children(current)
|
||||
|
||||
return current
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user