Compare commits

..

13 Commits

6 changed files with 496 additions and 431 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -3,34 +3,13 @@
display: none;
opacity: 0;
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 */
.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 {
/* 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;
}
}

View File

@ -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';
}
}
}
});

View File

@ -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 %}

View File

@ -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