Compare commits

...

13 Commits

9 changed files with 364 additions and 315 deletions

View File

@ -1,7 +1,7 @@
# Landingpage # Landingpage
## Access ## Access
### Locale
http://127.0.0.1:5000 http://127.0.0.1:5000
@ -19,12 +19,12 @@ docker build -t application-landingpage .
### Run ### Run
#### Development #### Run Development Environment
```bash ```bash
sudo docker run -d -p 5000:5000 --name landingpage -v $(pwd)/app/:/app -e FLASK_APP=app.py -e FLASK_ENV=development application-landingpage docker run -d -p 5000:5000 --name landingpage -v $(pwd)/app/:/app -e FLASK_APP=app.py -e FLASK_ENV=development application-landingpage
``` ```
#### Production #### Run Production Environment
```bash ```bash
docker run -d -p 5000:5000 --name landingpage application-landingpage docker run -d -p 5000:5000 --name landingpage application-landingpage
``` ```
@ -33,3 +33,5 @@ docker run -d -p 5000:5000 --name landingpage application-landingpage
```bash ```bash
docker logs -f landingpage docker logs -f landingpage
``` ```
## Author
This software was created from [Kevin Veen-Birkenbach](https://www.veen.world/) with the help of [ChatGPT]()

View File

@ -4,7 +4,6 @@ import requests
import hashlib import hashlib
import yaml import yaml
from utils.configuration_resolver import ConfigurationResolver from utils.configuration_resolver import ConfigurationResolver
from pprint import pprint
from utils.cache_manager import CacheManager from utils.cache_manager import CacheManager
# Initialize the CacheManager # Initialize the CacheManager
@ -35,9 +34,6 @@ FLASK_ENV = os.getenv("FLASK_ENV", "production")
def reload_config_in_dev(): def reload_config_in_dev():
if FLASK_ENV == "development": if FLASK_ENV == "development":
load_config(app) load_config(app)
print("DEVELOPMENT ENVIRONMENT")
else:
print("PRODUCTIVE ENVIRONMENT")
# Cache the icons # Cache the icons
for card in app.config["cards"]: for card in app.config["cards"]:

View File

@ -1,4 +1,88 @@
--- ---
accounts:
name: Accounts
description: My Online Accounts
icon:
class: fa-solid fa-users
subitems:
- name: Meta
description: Social and developer networks
icon:
class: fa-brands fa-meta
url:
subitems:
- name: 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.
- name: Facebook
description: Like my Facebook page
icon:
class: fa-brands fa-facebook
url: https://www.facebook.com/kevinveenbirkenbach
- link: navigation.header.contact.messenger
- name: Carreer Profiles
icon:
class: fa-solid fa-user-tie
subitems:
- name: XING
description: Visit my XING profile
icon:
class: bi bi-building
url: https://www.xing.com/profile/Kevin_VeenBirkenbach
- name: LinkedIn
description: Connect on LinkedIn
icon:
class: bi bi-linkedin
url: https://www.linkedin.com/in/kevinveenbirkenbach
- name: Sports
description: My sport activities
icon:
class: fa-solid fa-running
url:
subitems:
- name: Garmin
description: My Garmin activities
icon:
class: fa-solid fa-person-running
url: https://s.veen.world/garmin
- name: Eversports
description: My Eversports sessions
icon:
class: fa-solid fa-dumbbell
url: https://s.veen.world/eversports
- name: Duolingo
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
icon:
class: fa-brands fa-spotify
url: https://open.spotify.com/user/31vebfzbjf3p7oualis76qfpr5ty
- name: Patreon
description: Support me on Patreon
icon:
class: fa-brands fa-patreon
url: https://patreon.com/kevinveenbirkenbach
- name: Discourse
description: Follow me on Discourse
icon:
class: fa-brands fa-discourse
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
@ -123,25 +207,25 @@ navigation:
icon: icon:
class: fa-brands fa-mastodon class: fa-brands fa-mastodon
url: https://microblog.veen.world/@kevinveenbirkenbach url: https://microblog.veen.world/@kevinveenbirkenbach
subitems: []
- name: Pictures - name: Pictures
description: View my photo gallery description: View my photo gallery
icon: icon:
class: fa-solid fa-camera class: fa-solid fa-camera
url: https://picture.veen.world/kevinveenbirkenbach url: https://picture.veen.world/kevinveenbirkenbach
subitems: []
- name: Videos - name: Videos
description: Watch my videos description: Watch my videos
icon: icon:
class: fa-solid fa-video class: fa-solid fa-video
url: https://video.veen.world/a/kevinveenbirkenbach url: https://video.veen.world/a/kevinveenbirkenbach
subitems: []
- name: Blog - name: Blog
description: Read my blog description: Read my blog
icon: icon:
class: fa-solid fa-blog class: fa-solid fa-blog
url: https://blog.veen.world url: https://blog.veen.world
subitems: []
- name: Code - name: Code
icon: icon:
class: fa-solid fa-laptop-code class: fa-solid fa-laptop-code
@ -152,13 +236,13 @@ navigation:
icon: icon:
class: bi bi-github class: bi bi-github
url: https://github.com/kevinveenbirkenbach url: https://github.com/kevinveenbirkenbach
subitems: []
- name: Gitea - name: Gitea
description: Explore my code repositories description: Explore my code repositories
icon: icon:
class: fa-solid fa-code class: fa-solid fa-code
url: https://git.veen.world/kevinveenbirkenbach url: https://git.veen.world/kevinveenbirkenbach
subitems: []
- name: Contact - name: Contact
description: Get in touch description: Get in touch
icon: icon:
@ -171,25 +255,7 @@ navigation:
url: mailto:kevin@veen.world url: mailto:kevin@veen.world
identifier: kevin@veen.world identifier: kevin@veen.world
alternatives: alternatives:
- link: navigation.header.contact.matrix - link: navigation.header.contact.messenger.matrix
- name: Matrix
description: Chat with me on Matrix
icon:
class: fa-solid fa-cubes
identifier: "@kevinveenbirkenbach:veen.world"
info: |
#### Why Use Matrix?
Matrix is a secure, decentralized communication platform that ensures privacy and control over your data. Learn more about [Matrix](https://matrix.org/).
#### Privacy and Security
End-to-end encryption keeps your conversations private and secure.
#### Decentralized and Open
Matrix's federated network means you can host your own server or use any provider while staying connected.
#### A Movement for Digital Freedom
By using Matrix, you support open, transparent, and secure communication.
- name: Mobile - name: Mobile
description: Call me description: Call me
icon: icon:
@ -215,7 +281,28 @@ navigation:
#### Stand for Security #### Stand for Security
Using PGP is more than a tool—it's a statement about valuing freedom, privacy, and the security of digital communication. Explore the principles of secure communication with [privacy guides](https://privacyguides.org/). Using PGP is more than a tool—it's a statement about valuing freedom, privacy, and the security of digital communication. Explore the principles of secure communication with [privacy guides](https://privacyguides.org/).
- name: Messenger
description: Social and developer networks
icon:
class: fa-solid fa-comments
subitems:
- name: Matrix
description: Chat with me on Matrix
icon:
class: fa-solid fa-cubes
identifier: "@kevinveenbirkenbach:veen.world"
info: |
#### Why Use Matrix?
Matrix is a secure, decentralized communication platform that ensures privacy and control over your data. Learn more about [Matrix](https://matrix.org/).
#### Privacy and Security
End-to-end encryption keeps your conversations private and secure.
#### Decentralized and Open
Matrix's federated network means you can host your own server or use any provider while staying connected.
#### A Movement for Digital Freedom
By using Matrix, you support open, transparent, and secure communication.
- name: Signal - name: Signal
description: Message me on Signal description: Message me on Signal
icon: icon:
@ -223,7 +310,7 @@ navigation:
identifier: "+491781798023" identifier: "+491781798023"
warning: Signal is not hosted by me! warning: Signal is not hosted by me!
alternatives: alternatives:
- link: navigation.header.contact.matrix - link: navigation.header.contact.messenger.matrix
- name: Telegram - name: Telegram
description: Message me on Telegram description: Message me on Telegram
icon: icon:
@ -233,104 +320,29 @@ navigation:
identifier: kevinveenbirkenbach identifier: kevinveenbirkenbach
warning: Telegram is not hosted by me! warning: Telegram is not hosted by me!
alternatives: alternatives:
- link: navigation.header.contact.matrix - link: navigation.header.contact.messenger.matrix
- name: WhatsApp - name: WhatsApp
description: Chat with me on WhatsApp description: Chat with me on WhatsApp
icon: icon:
class: fa-brands fa-whatsapp class: fa-brands fa-whatsapp
url: https://wa.me/491781798023 url: https://wa.me/491781798023
identifier: "+491781798023" identifier: "+491781798023"
warning: | info: Consider using decentralized and privacy-respecting alternatives to maintain control over your data, improve security, and foster healthier online interactions.
⚠️ **Caution with Meta Services**
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.
📌 **Recommendation:** Consider using decentralized and privacy-respecting alternatives to maintain control over your data, improve security, and foster healthier online interactions.
alternatives: alternatives:
- link: navigation.header.contact.matrix - link: navigation.header.contact.messenger.matrix
- link: navigation.header.contact.messenger.signal
- link: navigation.header.contact.messenger.telegram
footer: footer:
- name: External Accounts - link: accounts
description: Me on other plattforms - name: Solution Hub
description: Curated collection of self hosted tools
icon: icon:
class: fa-solid fa-external-link-alt class: fa-solid fa-network-wired
subitems:
- name: Meta
description: Social and developer networks
icon:
class: fa-brands fa-meta
url: url:
subitems: subitems:
- name: Instagram
description: Follow me on Instagram
icon:
class: fa-brands fa-instagram
url: https://www.instagram.com/kevinveenbirkenbach/
- name: Facebook
description: Like my Facebook page
icon:
class: fa-brands fa-facebook
url: https://www.facebook.com/kevinveenbirkenbach
- name: Communication
description: Social and developer networks
icon:
class: fa-brands fa-meta
subitems:
- link: navigation.header.contact.whatsapp
- link: navigation.header.contact.signal
- link: navigation.header.contact.telegram
- name: Carreer Profiles
icon:
class: fa-solid fa-user-tie
subitems:
- name: XING
description: Visit my XING profile
icon:
class: bi bi-building
url: https://www.xing.com/profile/Kevin_VeenBirkenbach
subitems: []
- name: LinkedIn
description: Connect on LinkedIn
icon:
class: bi bi-linkedin
url: https://www.linkedin.com/in/kevinveenbirkenbach
subitems: []
- name: Sports
description: My sport activities
icon:
class: fa-solid fa-running
url:
subitems:
- name: Garmin
description: My Garmin activities
icon:
class: fa-solid fa-person-running
url: https://s.veen.world/garmin
subitems: []
- name: Eversports
description: My Eversports sessions
icon:
class: fa-solid fa-dumbbell
url: https://s.veen.world/eversports
subitems: []
- name: Duolingo
description: Learn with me on Duolingo
icon:
class: fa-solid fa-language
url: https://www.duolingo.com/profile/kevinbirkenbach
subitems: []
- name: Spotify
description: Listen to my playlists
icon:
class: fa-brands fa-spotify
url: https://open.spotify.com/user/31vebfzbjf3p7oualis76qfpr5ty
subitems: []
- name: Patreon
description: Support me on Patreon
icon:
class: fa-brands fa-patreon
url: https://patreon.com/kevinveenbirkenbach
subitems: []
- name: Community - name: Community
description: My presence in the Fediverse description: Tools to manage the community
icon: icon:
class: fa-solid fa-users class: fa-solid fa-users
subitems: subitems:
@ -338,122 +350,128 @@ navigation:
description: Join the discussion description: Join the discussion
icon: icon:
class: fa-brands fa-discourse class: fa-brands fa-discourse
url: https://forum.veen.world/u/kevinveenbirkenbach url: https://forum.veen.world/
subitems: [] - name: Learning Platform
description: Learn with my academy
icon:
class: fa-solid fa-graduation-cap
url: https://academy.veen.world/
- name: Newsletter - name: Newsletter
description: Subscribe to my newsletter description: Subscribe to my newsletter
icon: icon:
class: fa-solid fa-envelope-open-text class: fa-solid fa-envelope-open-text
url: https://newsletter.veen.world/subscription/form url: https://newsletter.veen.world/subscription/form
subitems: [] - name: Project Management
- name: Work Hub description: Project Management Tools
description: Curated collection of self hosted tools for work, organization, and
learning.
icon: icon:
class: fa-solid fa-toolbox class: fa-solid fa-chart-line
url:
subitems: subitems:
- name: Open Project - name: Open Project
description: Explore my projects description: Explore my projects
icon: icon:
class: fa-solid fa-chart-line class: fa-solid fa-tasks
url: https://project.veen.world/ url: https://project.veen.world/
subitems: []
- name: Taiga - name: Taiga
description: View my Kanban board description: View my Kanban board
icon: icon:
class: bi bi-clipboard2-check-fill class: bi bi-clipboard2-check-fill
url: https://kanban.veen.world/ url: https://kanban.veen.world/
subitems: []
- name: Matomo - name: Matomo
description: Analyze with Matomo description: Analyze with Matomo
icon: icon:
class: fa-solid fa-chart-simple class: fa-solid fa-chart-simple
url: https://matomo.veen.world/ url: https://matomo.veen.world/
subitems: []
- name: Baserow - name: Baserow
description: Organize with Baserow description: Organize with Baserow
icon: icon:
class: fa-solid fa-table class: fa-solid fa-table
url: https://baserow.veen.world/ url: https://baserow.veen.world/
subitems: [] - name: Communication
icon:
class: fa-solid fa-comments
subitems:
- name: Elements - name: Elements
description: Chat with me description: Chat with me
icon: icon:
class: fa-solid fa-comment class: fa-solid fa-comment
url: https://element.veen.world/ url: https://element.veen.world/
subitems: []
- name: Big Blue Button - name: Big Blue Button
description: Join live events description: Join live events
icon: icon:
class: fa-solid fa-video class: fa-solid fa-video
url: https://meet.veen.world/ url: https://meet.veen.world/
subitems: []
- name: Mailu - name: Mailu
description: Send me a mail description: Send me a mail
icon: icon:
class: fa-solid fa-envelope class: fa-solid fa-envelope
url: https://mail.veen.world/ url: https://mail.veen.world/
subitems: []
- name: Moodel
description: Learn with my academy
icon:
class: fa-solid fa-graduation-cap
url: https://academy.veen.world/
subitems: []
- name: Yourls - name: Yourls
description: Find my curated links description: Find my curated links
icon: icon:
class: bi bi-link class: bi bi-link
url: https://s.veen.world/admin/ url: https://s.veen.world/admin/
subitems: []
- name: Nextcloud - name: Nextcloud
description: Access my cloud storage description: Access my cloud storage
icon: icon:
class: fa-solid fa-cloud class: fa-solid fa-cloud
url: https://cloud.veen.world/ url: https://cloud.veen.world/
subitems: []
- name: About
description: All information about me
icon:
class: fa-solid fa-user
subitems:
- name: Logbooks - name: Logbooks
description: My activity logs description: My activity logs
icon: icon:
class: fa-solid fa-book class: fa-solid fa-book
url:
subitems: subitems:
- name: Skydiver - name: Skydiver
description: View my skydiving logs description: View my skydiving logs
icon: icon:
class: fa-solid fa-parachute-box class: fa-solid fa-parachute-box
url: https://s.veen.world/skydiverlog url: https://s.veen.world/skydiverlog
subitems: []
- name: Skipper - name: Skipper
description: See my sailing records description: See my sailing records
icon: icon:
class: fa-solid fa-sailboat class: fa-solid fa-sailboat
url: https://s.veen.world/meilenbuch url: https://s.veen.world/meilenbuch
subitems: []
- name: Diver - name: Diver
description: Check my diving logs description: Check my diving logs
icon: icon:
class: fa-solid fa-fish class: fa-solid fa-fish
url: https://s.veen.world/diverlog url: https://s.veen.world/diverlog
subitems: []
- name: Pilot - name: Pilot
description: Review my flight logs description: Review my flight logs
icon: icon:
class: fa-solid fa-plane class: fa-solid fa-plane
url: https://s.veen.world/pilotlog url: https://s.veen.world/pilotlog
subitems: []
- name: Nature - name: Nature
description: Explore my nature logs description: Explore my nature logs
icon: icon:
class: fa-solid fa-tree class: fa-solid fa-tree
url: https://s.veen.world/naturejournal url: https://s.veen.world/naturejournal
- name: Vita - name: Vita
description: View my CV and professional background description: View my CV
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
subitems: [] - name: Certificates
description: View my certifications and degrees
icon:
class: fa-solid fa-graduation-cap
url: https://s.veen.world/lebenslauf
- name: Imprint - name: Imprint
icon: icon:
class: fa-solid fa-scale-balanced class: fa-solid fa-scale-balanced

View File

@ -1,7 +1,10 @@
/* General link styles */
a { a {
text-decoration: none; text-decoration: none;
color: #000000; color: #000000;
} }
/* Header styles */
.header img { .header img {
float: right; float: right;
width: 100px; width: 100px;
@ -11,61 +14,72 @@ a {
.header h1 { .header h1 {
position: relative; position: relative;
} }
/* Equal-height container using flexbox */
.equal-height { .equal-height {
display: flex; display: flex;
flex: 1; flex: 1;
} }
/* 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;
}
.card-body { .card-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; /* Zentriert die Inhalte horizontal */ align-items: center; /* Center content horizontally */
text-align: center; /* Zentriert den Text */ text-align: center; /* Center text alignment */
} }
.card-icon { .card-icon {
display: flex; display: flex;
justify-content: center; /* Zentriert das Icon horizontal */ justify-content: center; /* Center the icon horizontally */
} }
.card-text, .card-text,
.card ul { .card ul {
text-align: left; /* Stellt sicher, dass der Text linksbündig ist */ text-align: left; /* Align text to the left */
} }
.card{ .card-column {
flex: 1; /* Stellt sicher, dass die Karten die ganze Höhe ihrer Container ausfüllen */
border-width: 3px;
/*border-color: #000000;*/
}
h3.card-title{
font-size: 1.3em;
}
.card .stretched-link{
font-size: 0.7em;
}
.card-column{
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px; padding-bottom: 12px;
} }
h3.footer-title{ .card .stretched-link {
font-size: 0.7em;
}
h3.card-title {
font-size: 1.3em; font-size: 1.3em;
} }
/* Footer styles */
.footer { .footer {
margin-top: 12px; margin-top: 12px;
text-align: center; text-align: center;
font-size: 0.7em; font-size: 0.7em;
} }
.footer p, h3{ .footer p,
margin: 0px; .footer h3 {
padding: 0px; margin: 0;
padding: 0;
} }
h3.footer-title {
font-size: 1.3em;
}
/* Dropdown menu styles */
.dropdown-menu { .dropdown-menu {
position: absolute !important; position: absolute !important;
} }
@ -73,40 +87,26 @@ h3.footer-title{
.dropdown-menu-footer { .dropdown-menu-footer {
position: absolute !important; position: absolute !important;
top: auto !important; top: auto !important;
bottom: 100%; /* Positioniert das Menü über dem Auslöser */ bottom: 100%; /* Positions the menu above the trigger */
transform: translateY(-10px); /* Optional: Sanfter Abstand */ transform: translateY(-10px); /* Optional spacing for smoother appearance */
} }
/* Dropdown submenu styles */
.dropdown-submenu { .dropdown-submenu {
position: relative; position: relative;
list-style: none; list-style: none;
} }
.navbar, .card {
border-radius: 5px; /* Runde Ecken */
border: 1px solid #ccc; /* Optionale Rahmenfarbe */
padding: 10px; /* Optionaler Abstand innen */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); /* Optionale Schatteneffekte */
color: #000000 !important;
background-color: #f9f9f9;
}
.navbar-nav {
padding: 0;
margin: 0;
}
/* Stellt sicher, dass Submenüs korrekt positioniert sind */
.dropdown-submenu {
position: relative;
}
.dropdown-submenu > .dropdown-menu { .dropdown-submenu > .dropdown-menu {
position: absolute; position: absolute;
top: 0; top: 0;
left: 100%; /* Positioniert das Submenü rechts vom Hauptmenü */ left: 100%; /* Default position: open to the right */
margin-top: -1px; margin-top: -1px;
z-index: 1050;
transition: opacity 0.3s ease-in-out; /* Smooth opacity transition */
} }
/* Handle collapse behavior for dropdowns */
.dropdown-menu.collapse { .dropdown-menu.collapse {
display: none; display: none;
} }
@ -115,7 +115,7 @@ h3.footer-title{
display: block; display: block;
} }
/* Standardmäßig sind die Submenüs ausgeblendet */ /* Ensure submenus are hidden by default */
.dropdown-submenu .dropdown-menu { .dropdown-submenu .dropdown-menu {
display: none; display: none;
opacity: 0; opacity: 0;
@ -125,15 +125,19 @@ h3.footer-title{
top: 0; top: 0;
} }
/* Beim Hover auf das Submenü-Element wird das Menü angezeigt */ /* Show submenu on hover */
.dropdown-submenu:hover > .dropdown-menu { .dropdown-submenu:hover > .dropdown-menu {
display: block; display: block;
opacity: 1; opacity: 1;
z-index: 1050;
} }
/* Um sicherzustellen, dass es nicht sofort verschwindet */ /* Ensure submenu remains visible when hovered over */
.dropdown-submenu:hover > .dropdown-menu:hover { .dropdown-submenu:hover > .dropdown-menu:hover {
display: block; display: block;
opacity: 1; opacity: 1;
} }
/* Handle dynamic submenu positioning */
.dropdown-submenu > .dropdown-menu[style*="right: 100%"] {
left: auto; /* Override left position for leftward opening */
}

View File

@ -9,6 +9,19 @@ document.addEventListener('DOMContentLoaded', () => {
clearTimeout(timeout); clearTimeout(timeout);
const menu = submenu.querySelector('.dropdown-menu'); const menu = submenu.querySelector('.dropdown-menu');
if (menu) { if (menu) {
// Dynamische Positionierung
const rect = menu.getBoundingClientRect();
const viewportWidth = window.innerWidth;
// Überprüfen, ob Platz nach rechts ist, sonst nach links öffnen
if (rect.right > viewportWidth) {
menu.style.left = 'auto';
menu.style.right = '100%';
} else {
menu.style.left = '100%';
menu.style.right = 'auto';
}
menu.style.display = 'block'; menu.style.display = 'block';
menu.style.opacity = '1'; menu.style.opacity = '1';
} }

View File

@ -7,8 +7,8 @@
<hr /> <hr />
<h3 class="card-title">{{ card.title }}</h3> <h3 class="card-title">{{ card.title }}</h3>
<p class="card-text">{{ card.text }}</p> <p class="card-text">{{ card.text }}</p>
{% if card.link %} {% if card.url %}
<a href="{{ card.link }}" class="mt-auto btn btn-light stretched-link" ><i class="fa-solid fa-globe"></i> {{ card.link_text }}</a> <a href="{{ card.url }}" class="mt-auto btn btn-light stretched-link" ><i class="fa-solid fa-globe"></i> {{ card.link_text }}</a>
{% else %} {% else %}
<i class="fa-solid fa-hourglass"></i> {{ card.link_text }} <i class="fa-solid fa-hourglass"></i> {{ card.link_text }}
{% endif %} {% endif %}

View File

@ -4,13 +4,17 @@
{% if subitem.subitems %} {% if subitem.subitems %}
<li class="dropdown-submenu position-relative"> <li class="dropdown-submenu position-relative">
<a class="dropdown-item dropdown-toggle" href="#" title="{{ subitem.description }}"> <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 }} <i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
{% else %}
<p>Missing icon in subitem: {{ subitem }}</p>
{% endif %}
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{{ render_subitems(subitem.subitems) }} {{ render_subitems(subitem.subitems) }}
</ul> </ul>
</li> </li>
{% elif subitem.identifier %} {% elif subitem.identifier or subitem.warning or subitem.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({{ subitem|tojson|safe }})' data-bs-toggle="tooltip" title="{{ subitem.description }}">
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }} <i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
@ -22,7 +26,7 @@
{% if subitem.icon is defined and subitem.icon.class is defined %} {% if subitem.icon is defined and subitem.icon.class is defined %}
<i class="{{ subitem.icon.class }}"></i> {{ subitem.name }} <i class="{{ subitem.icon.class }}"></i> {{ subitem.name }}
{% else %} {% else %}
<p>Fehlendes Icon im Subitem: {{ subitem }}</p> <p>Missing icon in subitem: {{ subitem }}</p>
{% endif %} {% endif %}
</a> </a>
</li> </li>
@ -33,9 +37,6 @@
<!-- Navigation Bar --> <!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid"> <div class="container-fluid">
<!--
<a class="navbar-brand" href="#">Navbar</a>
-->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav{{menu_type}}" aria-controls="navbarNav{{menu_type}}" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav{{menu_type}}" aria-controls="navbarNav{{menu_type}}" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
@ -53,7 +54,11 @@
<!-- 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" data-popper-placement="top" title="{{ item.description }}" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown{{ loop.index }}" role="button" data-bs-toggle="dropdown" data-bs-display="dynamic" data-popper-placement="top" title="{{ item.description }}" aria-expanded="false">
{% if item.icon is defined and item.icon.class is defined %}
<i class="{{ item.icon.class }}" data-bs-toggle="tooltip"></i> {{ item.name }} <i class="{{ item.icon.class }}" data-bs-toggle="tooltip"></i> {{ item.name }}
{% else %}
<p>Missing icon in item: {{ item }}</p>
{% endif %}
</a> </a>
<ul class="dropdown-menu dropdown-menu-{{menu_type}}" aria-labelledby="navbarDropdown{{ loop.index }}"> <ul class="dropdown-menu dropdown-menu-{{menu_type}}" aria-labelledby="navbarDropdown{{ loop.index }}">
{{ render_subitems(item.subitems) }} {{ render_subitems(item.subitems) }}

View File

@ -23,7 +23,6 @@ class CacheManager:
""" """
if not os.path.exists(self.cache_dir): if not os.path.exists(self.cache_dir):
os.makedirs(self.cache_dir) os.makedirs(self.cache_dir)
print(f"Created cache directory: {self.cache_dir}")
def clear_cache(self): def clear_cache(self):
""" """
@ -34,7 +33,6 @@ class CacheManager:
file_path = os.path.join(self.cache_dir, filename) file_path = os.path.join(self.cache_dir, filename)
if os.path.isfile(file_path): if os.path.isfile(file_path):
os.remove(file_path) os.remove(file_path)
print(f"Deleted: {file_path}")
def cache_file(self, file_url): def cache_file(self, file_url):
""" """

View File

@ -1,3 +1,4 @@
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.
@ -21,7 +22,9 @@ class ConfigurationResolver:
for key, value in list(current_config.items()): for key, value in list(current_config.items()):
if key == "link": if key == "link":
try: try:
target = self._find_entry(root_config, value.lower()) 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.clear()
current_config.update(target) current_config.update(target)
except Exception as e: except Exception as e:
@ -35,7 +38,18 @@ 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 _find_entry(self, config, path): 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):
return next(
(item for item in current if isinstance(item, dict) and item.get("name", "").lower() == part),
None
)
def _find_entry(self, config, path, subitems):
""" """
Finds an entry in the configuration by a dot-separated path. Finds an entry in the configuration by a dot-separated path.
Supports both dictionaries and lists with `subitems` navigation. Supports both dictionaries and lists with `subitems` navigation.
@ -45,10 +59,7 @@ class ConfigurationResolver:
for part in parts: for part in parts:
if isinstance(current, list): if isinstance(current, list):
# Look for a matching name in the list # Look for a matching name in the list
found = next( found = self._find_by_name(current,part)
(item for item in current if isinstance(item, dict) and item.get("name", "").lower() == part),
None
)
if found: if 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])}. "
@ -64,20 +75,22 @@ class ConfigurationResolver:
# 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)
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])}. "
f"Current dictionary: {current}" f"Current dictionary: {current}"
) )
else:
current = current[key] current = current[key]
else: else:
raise ValueError( raise ValueError(
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:
# Navigate into `subitems` if present current = self._get_subitems(current)
if isinstance(current, dict) and ("subitems" in current and current["subitems"]):
current = current["subitems"]
return current return current